supervisely 6.73.321__py3-none-any.whl → 6.73.322__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.
@@ -65,6 +65,7 @@ class AvailablePointcloudEpisodesConverters:
65
65
  SLY = "supervisely"
66
66
  BAG = "rosbag"
67
67
  LYFT = "lyft"
68
+ KITTI360 = "kitti360"
68
69
 
69
70
 
70
71
  class AvailableVolumeConverters:
@@ -7,3 +7,4 @@ from supervisely.convert.pointcloud_episodes.lyft.lyft_converter import LyftEpis
7
7
  from supervisely.convert.pointcloud_episodes.nuscenes_conv.nuscenes_converter import (
8
8
  NuscenesEpisodesConverter,
9
9
  )
10
+ from supervisely.convert.pointcloud_episodes.kitti_360.kitti_360_converter import KITTI360Converter
@@ -0,0 +1,242 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional, List
4
+ from supervisely import PointcloudEpisodeAnnotation, ProjectMeta, is_development, logger, ObjClass, ObjClassCollection
5
+ from supervisely.geometry.cuboid_3d import Cuboid3d
6
+ from supervisely.api.api import Api, ApiField
7
+ from supervisely.convert.base_converter import AvailablePointcloudEpisodesConverters
8
+ from supervisely.convert.pointcloud_episodes.kitti_360.kitti_360_helper import *
9
+ from supervisely.convert.pointcloud_episodes.pointcloud_episodes_converter import PointcloudEpisodeConverter
10
+ from supervisely.io.fs import (
11
+ file_exists,
12
+ get_file_name,
13
+ get_file_name_with_ext,
14
+ list_files_recursively,
15
+ silent_remove,
16
+ )
17
+ from supervisely.pointcloud_annotation.pointcloud_episode_frame_collection import PointcloudEpisodeFrameCollection
18
+ from supervisely.pointcloud_annotation.pointcloud_episode_object_collection import PointcloudEpisodeObjectCollection
19
+ from supervisely.pointcloud_annotation.pointcloud_episode_object import PointcloudEpisodeObject
20
+ from supervisely.pointcloud_annotation.pointcloud_episode_frame import PointcloudEpisodeFrame
21
+ from supervisely.pointcloud_annotation.pointcloud_figure import PointcloudFigure
22
+
23
+ class KITTI360Converter(PointcloudEpisodeConverter):
24
+
25
+ class Item:
26
+
27
+ def __init__(
28
+ self,
29
+ scene_name: str,
30
+ frame_paths: List[str],
31
+ ann_data: Annotation3D,
32
+ poses_path: str,
33
+ related_images: Optional[tuple] = None,
34
+ custom_data: Optional[dict] = None,
35
+ ):
36
+ self._scene_name = scene_name
37
+ self._frame_paths = frame_paths
38
+ self._ann_data = ann_data
39
+ self._poses_path = poses_path
40
+ self._related_images = related_images or []
41
+
42
+ self._type = "point_cloud_episode"
43
+ self._custom_data = custom_data if custom_data is not None else {}
44
+
45
+ def __init__(self, *args, **kwargs):
46
+ self._calib_path = None
47
+ super().__init__(*args, **kwargs)
48
+
49
+ def __str__(self) -> str:
50
+ return AvailablePointcloudEpisodesConverters.KITTI360
51
+
52
+ @property
53
+ def key_file_ext(self) -> str:
54
+ return ".bin"
55
+
56
+ def validate_format(self) -> bool:
57
+ try:
58
+ import kitti360scripts
59
+ except ImportError:
60
+ logger.warn("Please run 'pip install kitti360Scripts' to import KITTI-360 data.")
61
+ return False
62
+
63
+ self._items = []
64
+ subdirs = os.listdir(self._input_data)
65
+ if len(subdirs) == 1:
66
+ self._input_data = os.path.join(self._input_data, subdirs[0])
67
+
68
+ # * Get calibration path
69
+ calib_dir = next(iter([(Path(path).parent).as_posix() for path in list_files_recursively(self._input_data, [".txt"], None, True) if Path(path).stem.startswith("calib")]), None)
70
+ if calib_dir is None:
71
+ return False
72
+ self._calib_path = calib_dir
73
+
74
+ # * Get pointcloud files paths
75
+ velodyne_files = list_files_recursively(self._input_data, [".bin"], None, True)
76
+ if len(velodyne_files) == 0:
77
+ return False
78
+
79
+ # * Get annotation files paths and related images
80
+ boxes_ann_files = list_files_recursively(self._input_data, [".xml"], None, True)
81
+ if len(boxes_ann_files) == 0:
82
+ return False
83
+ rimage_files = list_files_recursively(self._input_data, [".png"], None, True)
84
+
85
+ kitti_anns = []
86
+ for ann_file in boxes_ann_files:
87
+ key_name = Path(ann_file).stem
88
+
89
+ # * Get pointcloud files
90
+ frame_paths = []
91
+ for path in velodyne_files:
92
+ if key_name in Path(path).parts:
93
+ frame_paths.append(path)
94
+ if len(frame_paths) == 0:
95
+ logger.warn("No frames found for name: %s", key_name)
96
+ continue
97
+
98
+ # * Get related images
99
+ rimages = []
100
+ for rimage in rimage_files:
101
+ path = Path(rimage)
102
+ if key_name in path.parts:
103
+ cam_name = path.parts[-3]
104
+ rimages.append((cam_name, rimage))
105
+
106
+ # * Get poses
107
+ poses_filter = (
108
+ lambda x: x.endswith("cam0_to_world.txt") and key_name in Path(x).parts
109
+ )
110
+ poses_path = next(
111
+ path
112
+ for path in list_files_recursively(self._input_data, [".txt"], None, True)
113
+ if poses_filter(path)
114
+ )
115
+ if poses_path is None:
116
+ logger.warn("No poses found for name: %s", key_name)
117
+ continue
118
+
119
+ # * Parse annotation
120
+ ann = Annotation3D(ann_file)
121
+ kitti_anns.append(ann)
122
+
123
+ self._items.append(
124
+ self.Item(key_name, frame_paths, ann, poses_path, rimages)
125
+ )
126
+
127
+ # * Get object class names for meta
128
+ obj_class_names = set()
129
+ for ann in kitti_anns:
130
+ for obj in ann.get_objects():
131
+ obj_class_names.add(obj.name)
132
+ obj_classes = [ObjClass(obj_class, Cuboid3d) for obj_class in obj_class_names]
133
+ self._meta = ProjectMeta(obj_classes=ObjClassCollection(obj_classes))
134
+ return self.items_count > 0
135
+
136
+ def to_supervisely(
137
+ self,
138
+ item,
139
+ meta: ProjectMeta,
140
+ renamed_classes: dict = {},
141
+ renamed_tags: dict = {},
142
+ static_transformations: StaticTransformations = None,
143
+ ) -> PointcloudEpisodeAnnotation:
144
+ static_transformations.set_cam2world(item._poses_path)
145
+
146
+ frame_cnt = len(item._frame_paths)
147
+ objs, frames = [], []
148
+
149
+ frame_idx_to_figures = {idx: [] for idx in range(frame_cnt)}
150
+ for obj in item._ann_data.get_objects():
151
+ pcd_obj = PointcloudEpisodeObject(meta.get_obj_class(obj.name))
152
+ objs.append(pcd_obj)
153
+
154
+ for idx in range(frame_cnt):
155
+ if obj.start_frame <= idx <= obj.end_frame:
156
+ tr_matrix = static_transformations.world_to_velo_transformation(obj, idx)
157
+ geom = convert_kitti_cuboid_to_supervisely_geometry(tr_matrix)
158
+ frame_idx_to_figures[idx].append(PointcloudFigure(pcd_obj, geom, idx))
159
+ for idx, figures in frame_idx_to_figures.items():
160
+ frame = PointcloudEpisodeFrame(idx, figures)
161
+ frames.append(frame)
162
+ obj_collection = PointcloudEpisodeObjectCollection(objs)
163
+ frame_collection = PointcloudEpisodeFrameCollection(frames)
164
+ return PointcloudEpisodeAnnotation(
165
+ frame_cnt, objects=obj_collection, frames=frame_collection
166
+ )
167
+
168
+ def upload_dataset(self, api: Api, dataset_id: int, batch_size: int = 1, log_progress=True):
169
+ meta, renamed_classes, renamed_tags = self.merge_metas_with_conflicts(api, dataset_id)
170
+
171
+ dataset_info = api.dataset.get_info_by_id(dataset_id)
172
+ if log_progress:
173
+ progress, progress_cb = self.get_progress(sum([len(item._frame_paths) for item in self._items]), "Converting pointcloud episodes...")
174
+ else:
175
+ progress_cb = None
176
+ static_transformations = StaticTransformations(self._calib_path)
177
+ scene_ds = dataset_info
178
+ multiple_items = self.items_count > 1
179
+ for item in self._items:
180
+ scene_ds = api.dataset.create(dataset_info.project_id, item._scene_name, parent_id=dataset_id) if multiple_items else dataset_info
181
+ frame_to_pcd_ids = {}
182
+ for idx, frame_path in enumerate(item._frame_paths):
183
+ # * Convert pointcloud from ".bin" to ".pcd"
184
+ pcd_path = str(Path(frame_path).with_suffix(".pcd"))
185
+ if file_exists(pcd_path):
186
+ logger.warning(f"Overwriting file with path: {pcd_path}")
187
+ convert_bin_to_pcd(frame_path, pcd_path)
188
+
189
+ # * Upload pointcloud
190
+ pcd_name = get_file_name_with_ext(pcd_path)
191
+ info = api.pointcloud_episode.upload_path(scene_ds.id, pcd_name, pcd_path, {"frame": idx})
192
+ pcd_id = info.id
193
+ frame_to_pcd_ids[idx] = pcd_id
194
+
195
+ # * Clean up
196
+ silent_remove(pcd_path)
197
+
198
+ if log_progress:
199
+ progress_cb(1)
200
+
201
+ # * Upload photocontext
202
+ rimage_jsons = []
203
+ cam_names = []
204
+ hashes = api.pointcloud_episode.upload_related_images(
205
+ [rimage_path for _, rimage_path in item._related_images]
206
+ )
207
+ for (cam_name, rimage_path), img, pcd_id in zip(
208
+ item._related_images, hashes, list(frame_to_pcd_ids.values())
209
+ ):
210
+ cam_num = int(cam_name[-1])
211
+ rimage_info = convert_calib_to_image_meta(
212
+ get_file_name(rimage_path), static_transformations, cam_num
213
+ )
214
+ image_json = {
215
+ ApiField.ENTITY_ID: pcd_id,
216
+ ApiField.NAME: cam_name,
217
+ ApiField.HASH: img,
218
+ ApiField.META: rimage_info[ApiField.META],
219
+ }
220
+ rimage_jsons.append(image_json)
221
+ cam_names.append(cam_name)
222
+ if rimage_jsons:
223
+ api.pointcloud_episode.add_related_images(rimage_jsons, cam_names)
224
+
225
+ # * Convert annotation and upload
226
+ try:
227
+ ann = self.to_supervisely(
228
+ item, meta, renamed_classes, renamed_tags, static_transformations
229
+ )
230
+ api.pointcloud_episode.annotation.append(scene_ds.id, ann, frame_to_pcd_ids)
231
+ except Exception as e:
232
+ logger.error(
233
+ f"Failed to upload annotation for scene: {scene_ds.name}. Error: {repr(e)}",
234
+ stack_info=False,
235
+ )
236
+ continue
237
+
238
+ logger.info(f"Dataset ID:{scene_ds.id} has been successfully uploaded.")
239
+
240
+ if log_progress:
241
+ if is_development():
242
+ progress.close()
@@ -0,0 +1,386 @@
1
+ from supervisely import logger
2
+ from supervisely.io.fs import get_file_name
3
+ from supervisely.geometry.cuboid_3d import Cuboid3d
4
+ from supervisely.geometry.point_3d import Vector3d
5
+ from supervisely.geometry.point import Point
6
+
7
+ from collections import defaultdict
8
+ import os
9
+ import numpy as np
10
+
11
+
12
+ MAX_N = 1000
13
+
14
+
15
+ def local2global(semanticId, instanceId):
16
+ globalId = semanticId * MAX_N + instanceId
17
+ if isinstance(globalId, np.ndarray):
18
+ return globalId.astype(np.int)
19
+ else:
20
+ return int(globalId)
21
+
22
+
23
+ def global2local(globalId):
24
+ semanticId = globalId // MAX_N
25
+ instanceId = globalId % MAX_N
26
+ if isinstance(globalId, np.ndarray):
27
+ return semanticId.astype(int), instanceId.astype(int)
28
+ else:
29
+ return int(semanticId), int(instanceId)
30
+
31
+
32
+ annotation2global = defaultdict()
33
+
34
+
35
+ # Abstract base class for annotation objects
36
+ class KITTI360Object:
37
+ from abc import ABCMeta
38
+
39
+ __metaclass__ = ABCMeta
40
+
41
+ def __init__(self):
42
+ from matplotlib import cm
43
+
44
+ # the label
45
+ self.label = ""
46
+
47
+ # colormap
48
+ self.cmap = cm.get_cmap("Set1")
49
+ self.cmap_length = 9
50
+
51
+ def getColor(self, idx):
52
+ if idx == 0:
53
+ return np.array([0, 0, 0])
54
+ return np.asarray(self.cmap(idx % self.cmap_length)[:3]) * 255.0
55
+
56
+ # def assignColor(self):
57
+ # from kitti360scripts.helpers.labels import id2label # pylint: disable=import-error
58
+
59
+ # if self.semanticId >= 0:
60
+ # self.semanticColor = id2label[self.semanticId].color
61
+ # if self.instanceId > 0:
62
+ # self.instanceColor = self.getColor(self.instanceId)
63
+ # else:
64
+ # self.instanceColor = self.semanticColor
65
+
66
+
67
+ # Class that contains the information of a single annotated object as 3D bounding box
68
+ class KITTI360Bbox3D(KITTI360Object):
69
+ # Constructor
70
+ def __init__(self):
71
+ KITTI360Object.__init__(self)
72
+ # the polygon as list of points
73
+ self.vertices = []
74
+ self.faces = []
75
+ self.lines = [
76
+ [0, 5],
77
+ [1, 4],
78
+ [2, 7],
79
+ [3, 6],
80
+ [0, 1],
81
+ [1, 3],
82
+ [3, 2],
83
+ [2, 0],
84
+ [4, 5],
85
+ [5, 7],
86
+ [7, 6],
87
+ [6, 4],
88
+ ]
89
+
90
+ # the ID of the corresponding object
91
+ self.semanticId = -1
92
+ self.instanceId = -1
93
+ self.annotationId = -1
94
+
95
+ # the window that contains the bbox
96
+ self.start_frame = -1
97
+ self.end_frame = -1
98
+
99
+ # timestamp of the bbox (-1 if statis)
100
+ self.timestamp = -1
101
+
102
+ # projected vertices
103
+ self.vertices_proj = None
104
+ self.meshes = []
105
+
106
+ # name
107
+ self.name = ""
108
+
109
+ def __str__(self):
110
+ return self.name
111
+
112
+ # def generateMeshes(self):
113
+ # self.meshes = []
114
+ # if self.vertices_proj:
115
+ # for fidx in range(self.faces.shape[0]):
116
+ # self.meshes.append(
117
+ # [
118
+ # Point(self.vertices_proj[0][int(x)], self.vertices_proj[1][int(x)])
119
+ # for x in self.faces[fidx]
120
+ # ]
121
+ # )
122
+
123
+ def parseOpencvMatrix(self, node):
124
+ rows = int(node.find("rows").text)
125
+ cols = int(node.find("cols").text)
126
+ data = node.find("data").text.split(" ")
127
+
128
+ mat = []
129
+ for d in data:
130
+ d = d.replace("\n", "")
131
+ if len(d) < 1:
132
+ continue
133
+ mat.append(float(d))
134
+ mat = np.reshape(mat, [rows, cols])
135
+ return mat
136
+
137
+ def parseVertices(self, child):
138
+ transform = self.parseOpencvMatrix(child.find("transform"))
139
+ R = transform[:3, :3]
140
+ T = transform[:3, 3]
141
+ vertices = self.parseOpencvMatrix(child.find("vertices"))
142
+ faces = self.parseOpencvMatrix(child.find("faces"))
143
+
144
+ vertices = np.matmul(R, vertices.transpose()).transpose() + T
145
+ self.vertices = vertices
146
+ self.faces = faces
147
+ self.R = R
148
+ self.T = T
149
+
150
+ self.transform = transform
151
+
152
+ def parseBbox(self, child):
153
+ from kitti360scripts.helpers.labels import kittiId2label # pylint: disable=import-error
154
+
155
+ semanticIdKITTI = int(child.find("semanticId").text)
156
+ self.semanticId = kittiId2label[semanticIdKITTI].id
157
+ self.instanceId = int(child.find("instanceId").text)
158
+ # self.name = str(child.find('label').text)
159
+ self.name = kittiId2label[semanticIdKITTI].name
160
+
161
+ self.start_frame = int(child.find("start_frame").text)
162
+ self.end_frame = int(child.find("end_frame").text)
163
+
164
+ self.timestamp = int(child.find("timestamp").text)
165
+
166
+ self.annotationId = int(child.find("index").text) + 1
167
+
168
+ global annotation2global
169
+ annotation2global[self.annotationId] = local2global(self.semanticId, self.instanceId)
170
+ self.parseVertices(child)
171
+
172
+ def parseStuff(self, child):
173
+ from kitti360scripts.helpers.labels import name2label # pylint: disable=import-error
174
+
175
+ classmap = {
176
+ "driveway": "parking",
177
+ "ground": "terrain",
178
+ "unknownGround": "ground",
179
+ "railtrack": "rail track",
180
+ }
181
+ label = child.find("label").text
182
+ if label in classmap.keys():
183
+ label = classmap[label]
184
+
185
+ self.start_frame = int(child.find("start_frame").text)
186
+ self.end_frame = int(child.find("end_frame").text)
187
+
188
+ self.semanticId = name2label[label].id
189
+ self.instanceId = 0
190
+ self.parseVertices(child)
191
+
192
+
193
+ # Class that contains the information of the point cloud a single frame
194
+ class KITTI360Point3D(KITTI360Object):
195
+ # Constructor
196
+ def __init__(self):
197
+ KITTI360Object.__init__(self)
198
+
199
+ self.vertices = []
200
+
201
+ self.vertices_proj = None
202
+
203
+ # the ID of the corresponding object
204
+ self.semanticId = -1
205
+ self.instanceId = -1
206
+ self.annotationId = -1
207
+
208
+ # name
209
+ self.name = ""
210
+
211
+ # color
212
+ self.semanticColor = None
213
+ self.instanceColor = None
214
+
215
+ def __str__(self):
216
+ return self.name
217
+
218
+ # def generateMeshes(self):
219
+ # pass
220
+
221
+
222
+ # Meta class for KITTI360Bbox3D
223
+ class Annotation3D:
224
+ def __init__(self, labelPath):
225
+ from kitti360scripts.helpers.labels import labels # pylint: disable=import-error
226
+ import xml.etree.ElementTree as ET
227
+
228
+ key_name = get_file_name(labelPath)
229
+ # load annotation
230
+ tree = ET.parse(labelPath)
231
+ root = tree.getroot()
232
+
233
+ self.objects = defaultdict(dict)
234
+
235
+ self.num_bbox = 0
236
+ for child in root:
237
+ if child.find("transform") is None:
238
+ continue
239
+ obj = KITTI360Bbox3D()
240
+ obj.parseBbox(child)
241
+ globalId = local2global(obj.semanticId, obj.instanceId)
242
+ self.objects[globalId][obj.timestamp] = obj
243
+ self.num_bbox += 1
244
+
245
+ globalIds = np.asarray(list(self.objects.keys()))
246
+ semanticIds, instanceIds = global2local(globalIds)
247
+ for label in labels:
248
+ if label.hasInstances:
249
+ print(f"{label.name:<30}:\t {(semanticIds==label.id).sum()}")
250
+ print(f"Loaded {len(globalIds)} instances")
251
+ print(f"Loaded {self.num_bbox} boxes")
252
+
253
+ def __call__(self, semanticId, instanceId, timestamp=None):
254
+ globalId = local2global(semanticId, instanceId)
255
+ if globalId in self.objects.keys():
256
+ # static object
257
+ if len(self.objects[globalId].keys()) == 1:
258
+ if -1 in self.objects[globalId].keys():
259
+ return self.objects[globalId][-1]
260
+ else:
261
+ return None
262
+ # dynamic object
263
+ else:
264
+ return self.objects[globalId][timestamp]
265
+ else:
266
+ return None
267
+
268
+ def get_objects(self):
269
+ return [list(obj.values())[0] for obj in self.objects.values()]
270
+
271
+ class StaticTransformations:
272
+ def __init__(self, calibrations_path):
273
+ import kitti360scripts.devkits.commons.loadCalibration as lc # pylint: disable=import-error
274
+
275
+ cam2velo_path = os.path.join(calibrations_path, "calib_cam_to_velo.txt")
276
+ self.cam2velo = lc.loadCalibrationRigid(cam2velo_path)
277
+ perspective_path = os.path.join(calibrations_path, "perspective.txt")
278
+ self.intrinsic_calibrations = lc.loadPerspectiveIntrinsic(perspective_path)
279
+ self.cam2world = None
280
+
281
+ def set_cam2world(self, cam2world_path):
282
+ if not os.path.isfile(cam2world_path):
283
+ logger.warn("Camera to world calibration file was not found")
284
+ return
285
+
286
+ cam2world_rows = np.loadtxt(cam2world_path)
287
+ cam2world_rigid = np.reshape(cam2world_rows[:, 1:], (-1, 4, 4))
288
+ frames_numbers = list(np.reshape(cam2world_rows[:, :1], (-1)).astype(int))
289
+ cam2world = {}
290
+
291
+ current_rigid = cam2world_rigid[0]
292
+
293
+ for frame_index in range(0, frames_numbers[-1]):
294
+ if frame_index in frames_numbers:
295
+ mapped_index = frames_numbers.index(frame_index)
296
+ current_rigid = cam2world_rigid[mapped_index]
297
+
298
+ # (Tr(cam -> world))
299
+ cam2world[frame_index] = current_rigid
300
+ self.cam2world = cam2world
301
+
302
+ def world_to_velo_transformation(self, obj, frame_index):
303
+ # rotate_z = Rotation.from_rotvec(np.pi * np.array([0, 0, 1])).as_matrix()
304
+ # rotate_z = np.hstack((rotate_z, np.asarray([[0, 0, 0]]).T))
305
+
306
+ # tr0(local -> fixed_coordinates_local)
307
+ tr0 = np.asarray([[0, -1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
308
+
309
+ # tr0(fixed_coordinates_local -> world)
310
+ tr1 = obj.transform
311
+
312
+ # tr2(world -> cam)
313
+ tr2 = np.linalg.inv(self.cam2world[frame_index])
314
+
315
+ # tr3(world -> cam)
316
+ tr3 = self.cam2velo
317
+
318
+ return tr3 @ tr2 @ tr1 @ tr0
319
+
320
+ def get_extrinsic_matrix(self):
321
+ return np.linalg.inv(self.cam2velo)[:3, :4]
322
+
323
+ def get_intrinsics_matrix(self, camera_num):
324
+ try:
325
+ matrix = self.intrinsic_calibrations[f"P_rect_0{camera_num}"][:3, :3]
326
+ return matrix
327
+ except KeyError:
328
+ logger.warn(f"Camera {camera_num} intrinsic matrix was not found")
329
+ return
330
+
331
+ def convert_kitti_cuboid_to_supervisely_geometry(tr_matrix):
332
+ import transforms3d # pylint: disable=import-error
333
+ from scipy.spatial.transform.rotation import Rotation
334
+
335
+ Tdash, Rdash, Zdash, _ = transforms3d.affines.decompose44(tr_matrix)
336
+
337
+ x, y, z = Tdash[0], Tdash[1], Tdash[2]
338
+ position = Vector3d(x, y, z)
339
+
340
+ rotation_angles = Rotation.from_matrix(Rdash).as_euler("xyz", degrees=False)
341
+ r_x, r_y, r_z = rotation_angles[0], rotation_angles[1], rotation_angles[2]
342
+
343
+ # Invert the bbox by adding π to the yaw while maintaining its degree relative to the world
344
+ rotation = Vector3d(r_x, r_y, r_z + np.pi)
345
+
346
+ w, h, l = Zdash[0], Zdash[1], Zdash[2]
347
+ dimension = Vector3d(w, h, l)
348
+
349
+ return Cuboid3d(position, rotation, dimension)
350
+
351
+ def convert_bin_to_pcd(src, dst):
352
+ import open3d as o3d # pylint: disable=import-error
353
+
354
+ try:
355
+ bin = np.fromfile(src, dtype=np.float32).reshape(-1, 4)
356
+ except ValueError as e:
357
+ raise Exception(
358
+ f"Incorrect data in the KITTI 3D pointcloud file: {src}. "
359
+ f"There was an error while trying to reshape the data into a 4-column matrix: {e}. "
360
+ "Please ensure that the binary file contains a multiple of 4 elements to be "
361
+ "successfully reshaped into a (N, 4) array.\n"
362
+ )
363
+ points = bin[:, 0:3]
364
+ intensity = bin[:, -1]
365
+ intensity_fake_rgb = np.zeros((intensity.shape[0], 3))
366
+ intensity_fake_rgb[:, 0] = intensity
367
+ pc = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(points))
368
+ pc.colors = o3d.utility.Vector3dVector(intensity_fake_rgb)
369
+ o3d.io.write_point_cloud(dst, pc)
370
+
371
+
372
+ def convert_calib_to_image_meta(image_name, static, cam_num):
373
+ intrinsic_matrix = static.get_intrinsics_matrix(cam_num)
374
+ extrinsic_matrix = static.get_extrinsic_matrix()
375
+
376
+ data = {
377
+ "name": image_name,
378
+ "meta": {
379
+ "deviceId": cam_num,
380
+ "sensorsData": {
381
+ "extrinsicMatrix": list(extrinsic_matrix.flatten().astype(float)),
382
+ "intrinsicMatrix": list(intrinsic_matrix.flatten().astype(float)),
383
+ },
384
+ },
385
+ }
386
+ return data
@@ -1347,6 +1347,7 @@ class Inference:
1347
1347
  source=images_np,
1348
1348
  settings=settings,
1349
1349
  )
1350
+ anns = self._exclude_duplicated_predictions(api, anns, settings, dataset_id, ids)
1350
1351
  results.extend(self._format_output(anns, slides_data))
1351
1352
  return results
1352
1353
 
@@ -1395,6 +1396,10 @@ class Inference:
1395
1396
  )
1396
1397
  self.cache.set_project_meta(output_project_id, output_project_meta)
1397
1398
 
1399
+ ann = self._exclude_duplicated_predictions(
1400
+ api, anns, settings, ds_info.id, [image_id], output_project_meta
1401
+ )[0]
1402
+
1398
1403
  logger.debug(
1399
1404
  "Uploading annotation...",
1400
1405
  extra={
@@ -1404,6 +1409,10 @@ class Inference:
1404
1409
  },
1405
1410
  )
1406
1411
  api.annotation.upload_ann(image_id, ann)
1412
+ else:
1413
+ ann = self._exclude_duplicated_predictions(
1414
+ api, anns, settings, image_info.dataset_id, [image_id]
1415
+ )[0]
1407
1416
 
1408
1417
  result = self._format_output(anns, slides_data)[0]
1409
1418
  if async_inference_request_uuid is not None and ann is not None:
@@ -1786,6 +1795,15 @@ class Inference:
1786
1795
  batch_results = []
1787
1796
  for i, ann in enumerate(anns):
1788
1797
  image_info: ImageInfo = images_infos_dict[image_ids_batch[i]]
1798
+ ds_info = dataset_infos_dict[image_info.dataset_id]
1799
+ meta = output_project_metas_dict.get(ds_info.project_id, None)
1800
+ iou = settings.get("existing_objects_iou_thresh")
1801
+ if meta is None and isinstance(iou, float) and iou > 0:
1802
+ meta = ProjectMeta.from_json(api.project.get_meta(ds_info.project_id))
1803
+ output_project_metas_dict[ds_info.project_id] = meta
1804
+ ann = self._exclude_duplicated_predictions(
1805
+ api, [ann], settings, ds_info.id, [image_info.id], meta
1806
+ )[0]
1789
1807
  batch_results.append(
1790
1808
  {
1791
1809
  "annotation": ann.to_json(),
@@ -2086,6 +2104,19 @@ class Inference:
2086
2104
  source=images_nps,
2087
2105
  settings=settings,
2088
2106
  )
2107
+ iou = settings.get("existing_objects_iou_thresh")
2108
+ if output_project_meta is None and isinstance(iou, float) and iou > 0:
2109
+ output_project_meta = ProjectMeta.from_json(
2110
+ api.project.get_meta(project_info.id)
2111
+ )
2112
+ anns = self._exclude_duplicated_predictions(
2113
+ api,
2114
+ anns,
2115
+ settings,
2116
+ dataset_info.id,
2117
+ [ii.id for ii in images_infos_batch],
2118
+ output_project_meta,
2119
+ )
2089
2120
  batch_results = []
2090
2121
  for i, ann in enumerate(anns):
2091
2122
  batch_results.append(
@@ -2935,7 +2966,9 @@ class Inference:
2935
2966
  parser = argparse.ArgumentParser(description="Run Inference Serving")
2936
2967
 
2937
2968
  # Positional args
2938
- parser.add_argument("mode", nargs="?", type=str, help="Mode of operation: 'deploy' or 'predict'")
2969
+ parser.add_argument(
2970
+ "mode", nargs="?", type=str, help="Mode of operation: 'deploy' or 'predict'"
2971
+ )
2939
2972
  parser.add_argument("input", nargs="?", type=str, help="Local path to input data")
2940
2973
 
2941
2974
  # Deploy args
@@ -3459,6 +3492,127 @@ class Inference:
3459
3492
  f"Checkpoint {checkpoint_url} not found in Team Files. Cannot set workflow input"
3460
3493
  )
3461
3494
 
3495
+ def _exclude_duplicated_predictions(
3496
+ self,
3497
+ api: Api,
3498
+ pred_anns: List[Annotation],
3499
+ settings: dict,
3500
+ dataset_id: int,
3501
+ gt_image_ids: List[int],
3502
+ meta: Optional[ProjectMeta] = None,
3503
+ ):
3504
+ """
3505
+ Filter out predictions that significantly overlap with ground truth (GT) objects.
3506
+
3507
+ This is a wrapper around the `_filter_duplicated_predictions_from_ann` method that does the following:
3508
+ - Checks inference settings for the IoU threshold (`existing_objects_iou_thresh`)
3509
+ - Gets ProjectMeta object if not provided
3510
+ - Downloads GT annotations for the specified image IDs
3511
+ - Filters out predictions that have an IoU greater than or equal to the specified threshold with any GT object
3512
+
3513
+ :param api: Supervisely API object
3514
+ :type api: Api
3515
+ :param pred_anns: List of Annotation objects containing predictions
3516
+ :type pred_anns: List[Annotation]
3517
+ :param settings: Inference settings
3518
+ :type settings: dict
3519
+ :param dataset_id: ID of the dataset containing the images
3520
+ :type dataset_id: int
3521
+ :param gt_image_ids: List of image IDs to filter predictions. All images should belong to the same dataset
3522
+ :type gt_image_ids: List[int]
3523
+ :param meta: ProjectMeta object
3524
+ :type meta: Optional[ProjectMeta]
3525
+ :return: List of Annotation objects containing filtered predictions
3526
+ :rtype: List[Annotation]
3527
+
3528
+ Notes:
3529
+ ------
3530
+ - Requires PyTorch and torchvision for IoU calculations
3531
+ - This method is useful for identifying new objects that aren't already annotated in the ground truth
3532
+ """
3533
+ iou = settings.get("existing_objects_iou_thresh")
3534
+ if isinstance(iou, float) and 0 < iou <= 1:
3535
+ if meta is None:
3536
+ ds = api.dataset.get_info_by_id(dataset_id)
3537
+ meta = ProjectMeta.from_json(api.project.get_meta(ds.project_id))
3538
+ gt_anns = api.annotation.download_json_batch(dataset_id, gt_image_ids)
3539
+ gt_anns = [Annotation.from_json(ann, meta) for ann in gt_anns]
3540
+ for i in range(0, len(pred_anns)):
3541
+ before = len(pred_anns[i].labels)
3542
+ with Timer() as timer:
3543
+ pred_anns[i] = self._filter_duplicated_predictions_from_ann(
3544
+ gt_anns[i], pred_anns[i], iou
3545
+ )
3546
+ after = len(pred_anns[i].labels)
3547
+ logger.debug(
3548
+ f"{[i]}: applied NMS with IoU={iou}. Before: {before}, After: {after}. Time: {timer.get_time():.3f}ms"
3549
+ )
3550
+ return pred_anns
3551
+
3552
+ def _filter_duplicated_predictions_from_ann(
3553
+ self, gt_ann: Annotation, pred_ann: Annotation, iou_threshold: float
3554
+ ) -> Annotation:
3555
+ """
3556
+ Filter out predictions that significantly overlap with ground truth annotations.
3557
+
3558
+ This function compares each prediction with ground truth annotations of the same class
3559
+ and removes predictions that have an IoU (Intersection over Union) greater than or equal
3560
+ to the specified threshold with any ground truth annotation. This is useful for identifying
3561
+ new objects that aren't already annotated in the ground truth.
3562
+
3563
+ :param gt_ann: Annotation object containing ground truth labels
3564
+ :type gt_ann: Annotation
3565
+ :param pred_ann: Annotation object containing prediction labels to be filtered
3566
+ :type pred_ann: Annotation
3567
+ :param iou_threshold: IoU threshold (0.0-1.0). Predictions with IoU >= threshold with any
3568
+ ground truth box of the same class will be removed
3569
+ :type iou_threshold: float
3570
+ :return: A new annotation object containing only predictions that don't significantly
3571
+ overlap with ground truth annotations
3572
+ :rtype: Annotation
3573
+
3574
+
3575
+ Notes:
3576
+ ------
3577
+ - Predictions with classes not present in ground truth will be kept
3578
+ - Requires PyTorch and torchvision for IoU calculations
3579
+ """
3580
+
3581
+ try:
3582
+ import torch
3583
+ from torchvision.ops import box_iou
3584
+
3585
+ except ImportError:
3586
+ raise ImportError("Please install PyTorch and torchvision to use this feature.")
3587
+
3588
+ def _to_tensor(geom):
3589
+ return torch.tensor([geom.left, geom.top, geom.right, geom.bottom]).float()
3590
+
3591
+ new_labels = []
3592
+ pred_cls_bboxes = defaultdict(list)
3593
+ for label in pred_ann.labels:
3594
+ pred_cls_bboxes[label.obj_class.name].append(label)
3595
+
3596
+ gt_cls_bboxes = defaultdict(list)
3597
+ for label in gt_ann.labels:
3598
+ if label.obj_class.name not in pred_cls_bboxes:
3599
+ continue
3600
+ gt_cls_bboxes[label.obj_class.name].append(label)
3601
+
3602
+ for name, pred in pred_cls_bboxes.items():
3603
+ gt = gt_cls_bboxes[name]
3604
+ if len(gt) == 0:
3605
+ new_labels.extend(pred)
3606
+ continue
3607
+ pred_bboxes = torch.stack([_to_tensor(l.geometry.to_bbox()) for l in pred]).float()
3608
+ gt_bboxes = torch.stack([_to_tensor(l.geometry.to_bbox()) for l in gt]).float()
3609
+ iou_matrix = box_iou(pred_bboxes, gt_bboxes)
3610
+ iou_matrix = iou_matrix.cpu().numpy()
3611
+ keep_indices = np.where(np.all(iou_matrix < iou_threshold, axis=1))[0]
3612
+ new_labels.extend([pred[i] for i in keep_indices])
3613
+
3614
+ return pred_ann.clone(labels=new_labels)
3615
+
3462
3616
 
3463
3617
  def _get_log_extra_for_inference_request(inference_request_uuid, inference_request: dict):
3464
3618
  log_extra = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: supervisely
3
- Version: 6.73.321
3
+ Version: 6.73.322
4
4
  Summary: Supervisely Python SDK.
5
5
  Home-page: https://github.com/supervisely/supervisely
6
6
  Author: Supervisely
@@ -565,7 +565,7 @@ supervisely/collection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
565
565
  supervisely/collection/key_indexed_collection.py,sha256=x2UVlkprspWhhae9oLUzjTWBoIouiWY9UQSS_MozfH0,37643
566
566
  supervisely/collection/str_enum.py,sha256=Zp29yFGvnxC6oJRYNNlXhO2lTSdsriU1wiGHj6ahEJE,1250
567
567
  supervisely/convert/__init__.py,sha256=ropgB1eebG2bfLoJyf2jp8Vv9UkFujaW3jVX-71ho1g,1353
568
- supervisely/convert/base_converter.py,sha256=eCFnvyoMI96rWjB5amFPZX2fI_TSdr__ruqxwQIbfFo,18537
568
+ supervisely/convert/base_converter.py,sha256=rRMIxY3h7cX5WAu_qn7w9vzRBcDB_jLZm5u_XQh7QG4,18563
569
569
  supervisely/convert/converter.py,sha256=tWxTDfFv7hwzQhUQrBxzfr6WP8FUGFX_ewg5T2HbUYo,8959
570
570
  supervisely/convert/image/__init__.py,sha256=JEuyaBiiyiYmEUYqdn8Mog5FVXpz0H1zFubKkOOm73I,1395
571
571
  supervisely/convert/image/image_converter.py,sha256=8vak8ZoKTN1ye2ZmCTvCZ605-Rw1AFLIEo7bJMfnR68,10426
@@ -634,10 +634,13 @@ supervisely/convert/pointcloud/ply/ply_helper.py,sha256=YfLiV9m6a4NNEMs0J32dmMTL
634
634
  supervisely/convert/pointcloud/sly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
635
635
  supervisely/convert/pointcloud/sly/sly_pointcloud_converter.py,sha256=r56Rwil-55cRnd0sIePFGrf_xXa-lKQSfwhEUrjOquk,5070
636
636
  supervisely/convert/pointcloud/sly/sly_pointcloud_helper.py,sha256=kOluL97FfCFfIvnUE_FeN8iQLMlwdiMR5gayorOGDXw,3968
637
- supervisely/convert/pointcloud_episodes/__init__.py,sha256=tzrN8kKCpa-0PNp6s1uVIoGse_VKGb45KzCCUSYlH5Y,457
637
+ supervisely/convert/pointcloud_episodes/__init__.py,sha256=LePLQFEjXwhXap2zOY9SVTbW_NMbxKYZKBjBdRLimKE,557
638
638
  supervisely/convert/pointcloud_episodes/pointcloud_episodes_converter.py,sha256=qULUzO96BvWgNVmyxSQ0pUPBPG3WHgUJuK_U7Z8NM-g,9428
639
639
  supervisely/convert/pointcloud_episodes/bag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
640
640
  supervisely/convert/pointcloud_episodes/bag/bag_converter.py,sha256=jzWKXoFUWu11d5WlPfT1hphCubYpq_lhQZmhh07xZdQ,1659
641
+ supervisely/convert/pointcloud_episodes/kitti_360/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
642
+ supervisely/convert/pointcloud_episodes/kitti_360/kitti_360_converter.py,sha256=ls3Pgf9WYTtaTzf6nLCL3gMjG6zZ_EAVKE5OJSFAOPc,10033
643
+ supervisely/convert/pointcloud_episodes/kitti_360/kitti_360_helper.py,sha256=EHyJTRfIpUC3lETJOCTI_OY4ddmT0eTFLMMhOvSeCm0,12372
641
644
  supervisely/convert/pointcloud_episodes/lyft/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
642
645
  supervisely/convert/pointcloud_episodes/lyft/lyft_converter.py,sha256=QXreWUJ-QhoWgLPqRxCayatYCCCuSV6Z2XCZKScrD3o,10419
643
646
  supervisely/convert/pointcloud_episodes/nuscenes_conv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -876,7 +879,7 @@ supervisely/nn/benchmark/visualization/widgets/table/__init__.py,sha256=47DEQpj8
876
879
  supervisely/nn/benchmark/visualization/widgets/table/table.py,sha256=atmDnF1Af6qLQBUjLhK18RMDKAYlxnsuVHMSEa5a-e8,4319
877
880
  supervisely/nn/inference/__init__.py,sha256=QFukX2ip-U7263aEPCF_UCFwj6EujbMnsgrXp5Bbt8I,1623
878
881
  supervisely/nn/inference/cache.py,sha256=q4F7ZRzZghNWSVFClXEIHNMNW4PK6xddYckCFUgyhCo,32027
879
- supervisely/nn/inference/inference.py,sha256=RJPTCd-y5FtQ234Zdbj7D6stsR3ZpVo8GLpiXXAr2Bg,158665
882
+ supervisely/nn/inference/inference.py,sha256=SqfIgohv0U3USQpHerzkrnfIeC7JKGeQA49Tocliu1k,165877
880
883
  supervisely/nn/inference/session.py,sha256=jmkkxbe2kH-lEgUU6Afh62jP68dxfhF5v6OGDfLU62E,35757
881
884
  supervisely/nn/inference/video_inference.py,sha256=8Bshjr6rDyLay5Za8IB8Dr6FURMO2R_v7aELasO8pR4,5746
882
885
  supervisely/nn/inference/gui/__init__.py,sha256=wCxd-lF5Zhcwsis-wScDA8n1Gk_1O00PKgDviUZ3F1U,221
@@ -1075,9 +1078,9 @@ supervisely/worker_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
1075
1078
  supervisely/worker_proto/worker_api_pb2.py,sha256=VQfi5JRBHs2pFCK1snec3JECgGnua3Xjqw_-b3aFxuM,59142
1076
1079
  supervisely/worker_proto/worker_api_pb2_grpc.py,sha256=3BwQXOaP9qpdi0Dt9EKG--Lm8KGN0C5AgmUfRv77_Jk,28940
1077
1080
  supervisely_lib/__init__.py,sha256=7-3QnN8Zf0wj8NCr2oJmqoQWMKKPKTECvjH9pd2S5vY,159
1078
- supervisely-6.73.321.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1079
- supervisely-6.73.321.dist-info/METADATA,sha256=yVJfg3OU_JHg5N-hBOHneb0i5S2tBLYZsVQ9sdn67Co,33596
1080
- supervisely-6.73.321.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1081
- supervisely-6.73.321.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1082
- supervisely-6.73.321.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1083
- supervisely-6.73.321.dist-info/RECORD,,
1081
+ supervisely-6.73.322.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1082
+ supervisely-6.73.322.dist-info/METADATA,sha256=tXlMoMRbbXrc18yQVTx6Ti09xSaTCC4TyKgLUoNIC_U,33596
1083
+ supervisely-6.73.322.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
1084
+ supervisely-6.73.322.dist-info/entry_points.txt,sha256=U96-5Hxrp2ApRjnCoUiUhWMqijqh8zLR03sEhWtAcms,102
1085
+ supervisely-6.73.322.dist-info/top_level.txt,sha256=kcFVwb7SXtfqZifrZaSE3owHExX4gcNYe7Q2uoby084,28
1086
+ supervisely-6.73.322.dist-info/RECORD,,