supervisely 6.73.273__py3-none-any.whl → 6.73.275__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.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

@@ -0,0 +1,305 @@
1
+ from os import path as osp
2
+ from pathlib import Path
3
+ from typing import Dict, Optional
4
+
5
+ import supervisely.convert.pointcloud_episodes.nuscenes_conv.nuscenes_helper as helpers
6
+ import supervisely.io.fs as fs
7
+ from supervisely._utils import is_development
8
+ from supervisely.annotation.obj_class import ObjClass
9
+ from supervisely.annotation.tag_meta import TagMeta, TagValueType
10
+ from supervisely.api.api import Api, ApiField
11
+ from supervisely.convert.base_converter import AvailablePointcloudConverters
12
+ from supervisely.convert.pointcloud_episodes.pointcloud_episodes_converter import (
13
+ PointcloudEpisodeConverter,
14
+ )
15
+ from supervisely.geometry.cuboid_3d import Cuboid3d
16
+ from supervisely.pointcloud_annotation.pointcloud_episode_annotation import (
17
+ PointcloudEpisodeAnnotation,
18
+ )
19
+ from supervisely.pointcloud_annotation.pointcloud_episode_frame import (
20
+ PointcloudEpisodeFrame,
21
+ )
22
+ from supervisely.pointcloud_annotation.pointcloud_episode_frame_collection import (
23
+ PointcloudEpisodeFrameCollection,
24
+ )
25
+ from supervisely.pointcloud_annotation.pointcloud_episode_object import (
26
+ PointcloudEpisodeObject,
27
+ )
28
+ from supervisely.pointcloud_annotation.pointcloud_episode_object_collection import (
29
+ PointcloudEpisodeObjectCollection,
30
+ )
31
+ from supervisely.pointcloud_annotation.pointcloud_episode_tag_collection import (
32
+ PointcloudEpisodeTagCollection,
33
+ )
34
+ from supervisely.pointcloud_annotation.pointcloud_figure import PointcloudFigure
35
+ from supervisely.project.project_meta import ProjectMeta
36
+ from supervisely.sly_logger import logger
37
+ from supervisely.tiny_timer import TinyTimer
38
+
39
+
40
+ class NuscenesEpisodesConverter(PointcloudEpisodeConverter):
41
+ """Converter for NuScenes pointcloud episodes format."""
42
+
43
+ def __init__(
44
+ self,
45
+ input_data: str,
46
+ labeling_interface: str,
47
+ upload_as_links: bool,
48
+ remote_files_map: Optional[Dict[str, str]] = None,
49
+ ):
50
+ super().__init__(input_data, labeling_interface, upload_as_links, remote_files_map)
51
+ self._nuscenes = None
52
+
53
+ def __str__(self) -> str:
54
+ return AvailablePointcloudConverters.NUSCENES
55
+
56
+ def validate_format(self) -> bool:
57
+ try:
58
+ from nuscenes import NuScenes
59
+ except ImportError:
60
+ logger.warning("Please, run 'pip install nuscenes-devkit' to import NuScenes data.")
61
+ return False
62
+
63
+ def filter_fn(path):
64
+ return all(
65
+ [
66
+ (Path(path) / name).exists()
67
+ for name in ["maps", "samples", "sweeps", "v1.0-mini"]
68
+ ]
69
+ )
70
+
71
+ try:
72
+ input_path = [d for d in fs.dirs_filter(self._input_data, filter_fn)].pop()
73
+ except IndexError:
74
+ return False
75
+
76
+ sample_dir = input_path + "/samples/"
77
+ if any([not fs.dir_exists(f"{sample_dir}/{d}") for d in helpers.DIR_NAMES]):
78
+ return False
79
+
80
+ sweeps_dir = input_path + "/sweeps/"
81
+ if any([not fs.dir_exists(f"{sweeps_dir}/{d}") for d in helpers.DIR_NAMES]):
82
+ return False
83
+
84
+ ann_dir = input_path + "/v1.0-mini/"
85
+ if any([not fs.file_exists(f"{ann_dir}/{d}.json") for d in helpers.TABLE_NAMES]):
86
+ return False
87
+
88
+ try:
89
+ t = TinyTimer()
90
+ nuscenes = NuScenes(dataroot=input_path, verbose=False)
91
+ self._nuscenes: NuScenes = nuscenes
92
+ logger.info(f"NuScenes initialization took {t.get_sec():.3f} sec")
93
+ except Exception as e:
94
+ logger.debug(f"Failed to initialize NuScenes: {e}")
95
+ return False
96
+
97
+ return True
98
+
99
+ def to_supervisely(
100
+ self,
101
+ scene_samples,
102
+ meta: ProjectMeta,
103
+ renamed_classes: dict = {},
104
+ renamed_tags: dict = {},
105
+ ) -> PointcloudEpisodeAnnotation:
106
+ token_to_obj = {}
107
+ frames = []
108
+ tags = []
109
+ for sample_i, sample in enumerate(scene_samples):
110
+ figures = []
111
+ for obj in sample.anns:
112
+ instance_token = obj.instance_token
113
+ class_name = obj.category
114
+ parent_obj_token = obj.parent_token
115
+ parent_object = None
116
+ if parent_obj_token == "":
117
+ # * Create a new object
118
+ obj_class_name = renamed_classes.get(class_name, class_name)
119
+ obj_class = meta.get_obj_class(obj_class_name)
120
+ obj_tags = None # ! TODO: fix tags
121
+ pcd_ep_obj = PointcloudEpisodeObject(obj_class, obj_tags)
122
+ # * Assign the object to the starting token
123
+ token_to_obj[instance_token] = pcd_ep_obj
124
+ parent_object = pcd_ep_obj
125
+ else:
126
+ # * -> Figure has a parent object, get it
127
+ token_to_obj[instance_token] = token_to_obj[parent_obj_token]
128
+ parent_object = token_to_obj[parent_obj_token]
129
+ geom = obj.to_supervisely()
130
+ pcd_figure = PointcloudFigure(parent_object, geom, sample_i)
131
+ figures.append(pcd_figure)
132
+ frame = PointcloudEpisodeFrame(sample_i, figures)
133
+ frames.append(frame)
134
+ tag_collection = PointcloudEpisodeTagCollection(tags) if len(tags) > 0 else None
135
+ return PointcloudEpisodeAnnotation(
136
+ len(frames),
137
+ PointcloudEpisodeObjectCollection(list(set(token_to_obj.values()))),
138
+ PointcloudEpisodeFrameCollection(frames),
139
+ tag_collection,
140
+ )
141
+
142
+ def upload_dataset(self, api: Api, dataset_id: int, batch_size: int = 1, log_progress=True):
143
+ nuscenes = self._nuscenes
144
+
145
+ tag_metas = [TagMeta(attr["name"], TagValueType.NONE) for attr in nuscenes.attribute]
146
+ obj_classes = []
147
+ for category in nuscenes.category:
148
+ color = nuscenes.colormap[category["name"]]
149
+ description = category["description"]
150
+ if len(description) > 255:
151
+ # * Trim description to fit into 255 characters limit
152
+ sentences = description.split(".")
153
+ trimmed_description = ""
154
+ for sentence in sentences:
155
+ if len(trimmed_description) + len(sentence) + 1 > 255:
156
+ break
157
+ trimmed_description += sentence + "."
158
+ description = trimmed_description.strip()
159
+ obj_classes.append(ObjClass(category["name"], Cuboid3d, color, description=description))
160
+
161
+ self._meta = ProjectMeta(obj_classes, tag_metas)
162
+ meta, renamed_classes, renamed_tags = self.merge_metas_with_conflicts(api, dataset_id)
163
+
164
+ dataset_info = api.dataset.get_info_by_id(dataset_id)
165
+ scene_name_to_dataset = {}
166
+
167
+ scene_names = [scene["name"] for scene in nuscenes.scene]
168
+ scene_cnt = len(scene_names)
169
+ total_sample_cnt = sum([scene["nbr_samples"] for scene in nuscenes.scene])
170
+
171
+ multiple_scenes = len(scene_names) > 1
172
+ if multiple_scenes:
173
+ logger.info(f"Found {scene_cnt} scenes ({total_sample_cnt} samples) in the input data.")
174
+ # * Create a nested dataset for each scene
175
+ for name in scene_names:
176
+ ds = api.dataset.create(
177
+ dataset_info.project_id,
178
+ name,
179
+ change_name_if_conflict=True,
180
+ parent_id=dataset_id,
181
+ )
182
+ scene_name_to_dataset[name] = ds
183
+ else:
184
+ scene_name_to_dataset[scene_names[0]] = dataset_info
185
+
186
+ if log_progress:
187
+ progress, progress_cb = self.get_progress(total_sample_cnt, "Converting episode scenes...")
188
+ else:
189
+ progress_cb = None
190
+
191
+ for scene in nuscenes.scene:
192
+ current_dataset_id = scene_name_to_dataset[scene["name"]].id
193
+
194
+ log = nuscenes.get("log", scene["log_token"])
195
+ sample_token = scene["first_sample_token"]
196
+
197
+ # * Extract scene's samples
198
+ scene_samples = []
199
+ for i in range(scene["nbr_samples"]):
200
+ sample = nuscenes.get("sample", sample_token)
201
+ lidar_path, boxes, _ = nuscenes.get_sample_data(sample["data"]["LIDAR_TOP"])
202
+ if not osp.exists(lidar_path):
203
+ logger.warning(f'Scene "{scene["name"]}" has no LIDAR data.')
204
+ continue
205
+
206
+ timestamp = sample["timestamp"]
207
+ anns = []
208
+ for box, name, inst_token in helpers.Sample.generate_boxes(nuscenes, boxes):
209
+ current_instance_token = inst_token["token"]
210
+ parent_token = inst_token["prev"]
211
+
212
+ # get category, attributes and visibility
213
+ ann = nuscenes.get("sample_annotation", current_instance_token)
214
+ category = ann["category_name"]
215
+ attributes = [
216
+ nuscenes.get("attribute", attr)["name"] for attr in ann["attribute_tokens"]
217
+ ]
218
+ visibility = nuscenes.get("visibility", ann["visibility_token"])["level"]
219
+
220
+ anns.append(
221
+ helpers.AnnotationObject(
222
+ name,
223
+ box,
224
+ current_instance_token,
225
+ parent_token,
226
+ category,
227
+ attributes,
228
+ visibility,
229
+ )
230
+ )
231
+
232
+ # get camera data
233
+ sample_data = nuscenes.get("sample_data", sample["data"]["LIDAR_TOP"])
234
+ cal_sensor = nuscenes.get(
235
+ "calibrated_sensor", sample_data["calibrated_sensor_token"]
236
+ )
237
+ ego_pose = nuscenes.get("ego_pose", sample_data["ego_pose_token"])
238
+
239
+ camera_data = [
240
+ helpers.CamData(nuscenes, sensor, token, cal_sensor, ego_pose)
241
+ for sensor, token in sample["data"].items()
242
+ if sensor.startswith("CAM")
243
+ ]
244
+ scene_samples.append(helpers.Sample(timestamp, lidar_path, anns, camera_data))
245
+ sample_token = sample["next"]
246
+
247
+ # * Convert and upload pointclouds
248
+ frame_to_pointcloud_ids = {}
249
+ for idx, sample in enumerate(scene_samples):
250
+ pcd_path = sample.convert_lidar_to_supervisely()
251
+
252
+ pcd_name = fs.get_file_name(pcd_path)
253
+ pcd_meta = {
254
+ "frame": idx,
255
+ "vehicle": log["vehicle"],
256
+ "date": log["date_captured"],
257
+ "location": log["location"],
258
+ "description": scene["description"],
259
+ }
260
+ info = api.pointcloud_episode.upload_path(
261
+ current_dataset_id, pcd_name, pcd_path, pcd_meta
262
+ )
263
+ fs.silent_remove(pcd_path)
264
+
265
+ pcd_id = info.id
266
+ frame_to_pointcloud_ids[idx] = pcd_id
267
+
268
+ # * Upload related images
269
+ image_jsons = []
270
+ camera_names = []
271
+ for img_path, rimage_info in [
272
+ data.get_info(sample.timestamp) for data in sample.cam_data
273
+ ]:
274
+ img = api.pointcloud_episode.upload_related_image(img_path)
275
+ image_jsons.append(
276
+ {
277
+ ApiField.ENTITY_ID: pcd_id,
278
+ ApiField.NAME: rimage_info[ApiField.NAME],
279
+ ApiField.HASH: img,
280
+ ApiField.META: rimage_info[ApiField.META],
281
+ }
282
+ )
283
+ camera_names.append(rimage_info[ApiField.META]["deviceId"])
284
+ if len(image_jsons) > 0:
285
+ api.pointcloud_episode.add_related_images(image_jsons, camera_names)
286
+
287
+ if log_progress:
288
+ progress_cb(1)
289
+
290
+ # * Convert and upload annotations
291
+ pcd_ann = self.to_supervisely(scene_samples, meta, renamed_classes, renamed_tags)
292
+ try:
293
+ api.pointcloud_episode.annotation.append(
294
+ current_dataset_id, pcd_ann, frame_to_pointcloud_ids
295
+ )
296
+ logger.info(f"Dataset ID:{current_dataset_id} has been successfully uploaded.")
297
+ except Exception as e:
298
+ error_msg = getattr(getattr(e, "response", e), "text", str(e))
299
+ logger.warning(
300
+ f"Failed to upload annotation for scene: {scene['name']}. Message: {error_msg}"
301
+ )
302
+
303
+ if log_progress:
304
+ if is_development():
305
+ progress.close()
@@ -0,0 +1,265 @@
1
+ from datetime import datetime
2
+ from os import path as osp
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ import numpy as np
7
+
8
+ from supervisely import fs, logger
9
+ from supervisely.geometry.cuboid_3d import Cuboid3d, Vector3d
10
+
11
+ DIR_NAMES = [
12
+ "CAM_BACK",
13
+ "CAM_BACK_LEFT",
14
+ "CAM_BACK_RIGHT",
15
+ "CAM_FRONT",
16
+ "CAM_FRONT_LEFT",
17
+ "CAM_FRONT_RIGHT",
18
+ "LIDAR_TOP",
19
+ "RADAR_FRONT",
20
+ "RADAR_FRONT_LEFT",
21
+ "RADAR_FRONT_RIGHT",
22
+ "RADAR_BACK_LEFT",
23
+ "RADAR_BACK_RIGHT",
24
+ ]
25
+
26
+ TABLE_NAMES = [
27
+ "category",
28
+ "attribute",
29
+ "visibility",
30
+ "instance",
31
+ "sensor",
32
+ "calibrated_sensor",
33
+ "ego_pose",
34
+ "log",
35
+ "scene",
36
+ "sample",
37
+ "sample_data",
38
+ "sample_annotation",
39
+ "map",
40
+ ]
41
+
42
+
43
+ class Sample:
44
+ """
45
+ A class to represent a sample from the NuScenes dataset.
46
+ """
47
+
48
+ def __init__(self, timestamp, lidar_path, anns, cam_data):
49
+ self.timestamp = datetime.utcfromtimestamp(timestamp / 1e6).isoformat()
50
+ self.lidar_path = lidar_path
51
+ self.anns = anns
52
+ self.cam_data = cam_data
53
+
54
+ @staticmethod
55
+ def generate_boxes(nuscenes, boxes):
56
+ """
57
+ Generate ground truth boxes for a given set of boxes.
58
+
59
+ Yields:
60
+ tuple: A tuple containing:
61
+ - gt_box (np.ndarray): A numpy array representing the ground truth box with concatenated location,
62
+ dimensions, and rotation.
63
+ - name (str): The name of the object.
64
+ - instance_token (str): The instance token associated with the box.
65
+ """
66
+ locs = np.array([b.center for b in boxes]).reshape(-1, 3)
67
+ dims = np.array([b.wlh for b in boxes]).reshape(-1, 3)
68
+ rots = np.array([b.orientation.yaw_pitch_roll[0] for b in boxes]).reshape(-1, 1)
69
+
70
+ gt_boxes = np.concatenate([locs, dims, -rots - np.pi / 2], axis=1)
71
+ names = np.array([b.name for b in boxes])
72
+ instance_tokens = [nuscenes.get("sample_annotation", box.token) for box in boxes]
73
+
74
+ yield from zip(gt_boxes, names, instance_tokens)
75
+
76
+ def convert_lidar_to_supervisely(self):
77
+ """
78
+ Converts a LiDAR point cloud file to the Supervisely format and saves it as a .pcd file.
79
+
80
+ Returns:
81
+ str: The file path of the saved .pcd file.
82
+ """
83
+ import open3d as o3d # pylint: disable=import-error
84
+
85
+ bin_file = Path(self.lidar_path)
86
+ save_path = str(bin_file.with_suffix(".pcd"))
87
+
88
+ b = np.fromfile(bin_file, dtype=np.float32).reshape(-1, 5)
89
+ points = b[:, 0:3]
90
+ intensity = b[:, 3]
91
+ ring_index = b[:, 4]
92
+ intensity_fake_rgb = np.zeros((intensity.shape[0], 3))
93
+ intensity_fake_rgb[:, 0] = (
94
+ intensity # red The intensity measures the reflectivity of the objects
95
+ )
96
+ intensity_fake_rgb[:, 1] = (
97
+ ring_index # green ring index is the index of the laser ranging from 0 to 31
98
+ )
99
+ try:
100
+ pc = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(points))
101
+ pc.colors = o3d.utility.Vector3dVector(intensity_fake_rgb)
102
+ o3d.io.write_point_cloud(save_path, pc)
103
+ except Exception as e:
104
+ logger.warning(f"Error converting lidar to supervisely format: {e}")
105
+ return save_path
106
+
107
+
108
+ class AnnotationObject:
109
+ """
110
+ A class to represent an annotation object in the NuScenes dataset.
111
+
112
+ Attributes:
113
+ -----------
114
+ name : str
115
+ The name of the annotation object.
116
+ bbox : np.ndarray
117
+ The bounding box coordinates.
118
+ instance_token : str
119
+ The instance token associated with the annotation object.
120
+ parent_token : str
121
+ The token of instance preceding the current object instance.
122
+ category : str
123
+ The class name of the annotation object.
124
+ attributes : List[str]
125
+ The attribute names associated with the annotation object.
126
+ visibility : str
127
+ The visibility level of the annotation object.
128
+ """
129
+
130
+ def __init__(
131
+ self,
132
+ name: str,
133
+ bbox: np.ndarray,
134
+ instance_token: str,
135
+ parent_token: str,
136
+ category: str,
137
+ attributes: List[str],
138
+ visibility: str,
139
+ ):
140
+ self.name = name
141
+ self.bbox = bbox
142
+ self.instance_token = instance_token
143
+ self.parent_token = parent_token
144
+
145
+ self.category = category
146
+ self.attributes = attributes
147
+ self.visibility = visibility
148
+
149
+ def to_supervisely(self):
150
+ box = self.convert_nuscenes_to_BEVBox3D()
151
+
152
+ bbox = box.to_xyzwhlr()
153
+ dim = bbox[[3, 5, 4]]
154
+ pos = bbox[:3] + [0, 0, dim[1] / 2]
155
+ yaw = bbox[-1]
156
+
157
+ position = Vector3d(float(pos[0]), float(pos[1]), float(pos[2]))
158
+ rotation = Vector3d(0, 0, float(-yaw))
159
+ dimension = Vector3d(float(dim[0]), float(dim[2]), float(dim[1]))
160
+ geometry = Cuboid3d(position, rotation, dimension)
161
+
162
+ return geometry
163
+
164
+ def convert_nuscenes_to_BEVBox3D(self):
165
+ import open3d as o3d # pylint: disable=import-error
166
+
167
+ box = self.bbox
168
+ center = [float(box[0]), float(box[1]), float(box[2])]
169
+ size = [float(box[3]), float(box[5]), float(box[4])]
170
+ ry = float(box[6])
171
+ yaw = ry - np.pi
172
+ yaw = yaw - np.floor(yaw / (2 * np.pi) + 0.5) * 2 * np.pi
173
+ world_cam = None
174
+ return o3d.ml.datasets.utils.BEVBox3D(center, size, yaw, self.name, -1.0, world_cam)
175
+
176
+
177
+ class CamData:
178
+ """
179
+ A class to represent camera data and perform transformations between different coordinate systems.
180
+
181
+ Attributes:
182
+ -----------
183
+ name : str
184
+ The name of the sensor.
185
+ path : str
186
+ The path to the image file.
187
+ imsize : tuple
188
+ The size of the image (width, height).
189
+ extrinsic : np.ndarray
190
+ The extrinsic matrix (4x4) representing the transformation from the lidar to the camera coordinate system.
191
+ intrinsic : np.ndarray
192
+ The intrinsic matrix (3x3) representing the camera's intrinsic parameters.
193
+ """
194
+
195
+ def __init__(self, nuscenes, sensor_name, sensor_token, cs_record, ego_record):
196
+ from nuscenes.utils.data_classes import ( # pylint: disable=import-error
197
+ transform_matrix,
198
+ )
199
+ from pyquaternion import Quaternion # pylint: disable=import-error
200
+
201
+ img_path, boxes, cam_intrinsic = nuscenes.get_sample_data(sensor_token)
202
+ if not osp.exists(img_path):
203
+ return None
204
+
205
+ sd_record_cam = nuscenes.get("sample_data", sensor_token)
206
+ cs_record_cam = nuscenes.get("calibrated_sensor", sd_record_cam["calibrated_sensor_token"])
207
+ ego_record_cam = nuscenes.get("ego_pose", sd_record_cam["ego_pose_token"])
208
+ lid_to_ego = transform_matrix(
209
+ cs_record["translation"],
210
+ Quaternion(cs_record["rotation"]),
211
+ inverse=False,
212
+ )
213
+ lid_ego_to_world = transform_matrix(
214
+ ego_record["translation"],
215
+ Quaternion(ego_record["rotation"]),
216
+ inverse=False,
217
+ )
218
+ world_to_cam_ego = transform_matrix(
219
+ ego_record_cam["translation"],
220
+ Quaternion(ego_record_cam["rotation"]),
221
+ inverse=True,
222
+ )
223
+ ego_to_cam = transform_matrix(
224
+ cs_record_cam["translation"],
225
+ Quaternion(cs_record_cam["rotation"]),
226
+ inverse=True,
227
+ )
228
+ velo_to_cam = np.dot(
229
+ ego_to_cam, np.dot(world_to_cam_ego, np.dot(lid_ego_to_world, lid_to_ego))
230
+ )
231
+ velo_to_cam_rot = velo_to_cam[:3, :3]
232
+ velo_to_cam_trans = velo_to_cam[:3, 3]
233
+
234
+ self.name = sensor_name
235
+ self.path = str(img_path)
236
+ self.imsize = (sd_record_cam["width"], sd_record_cam["height"])
237
+ self.extrinsic = np.hstack((velo_to_cam_rot, velo_to_cam_trans.reshape(3, 1)))
238
+ self.intrinsic = np.asarray(cs_record_cam["camera_intrinsic"])
239
+
240
+ def get_info(self, timestamp):
241
+ """
242
+ Retrieves information about the image and its metadata.
243
+
244
+ Args:
245
+ timestamp (int): The timestamp associated with the image.
246
+
247
+ Returns:
248
+ tuple: A tuple containing the image path and a dictionary with image metadata.
249
+ """
250
+ sensors_to_skip = ["_intrinsic", "_extrinsic", "_imsize"]
251
+ if not any([self.name.endswith(s) for s in sensors_to_skip]):
252
+ image_name = fs.get_file_name_with_ext(self.path)
253
+ sly_path_img = osp.join(osp.dirname(self.path), image_name)
254
+ img_info = {
255
+ "name": image_name,
256
+ "meta": {
257
+ "deviceId": self.name,
258
+ "timestamp": timestamp,
259
+ "sensorsData": {
260
+ "extrinsicMatrix": list(self.extrinsic.flatten().astype(float)),
261
+ "intrinsicMatrix": list(self.intrinsic.flatten().astype(float)),
262
+ },
263
+ },
264
+ }
265
+ return (sly_path_img, img_info)