""" nuScenes Dataset Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com), Zheng Zhang Please cite our work if the code is helpful to you. """ import os import numpy as np from collections.abc import Sequence import pickle from PIL import Image import open3d as o3d import torch from .builder import DATASETS from .defaults import DefaultDataset, DefaultImagePointDataset os.environ["OMP_NUM_THREADS"] = "1" @DATASETS.register_module() class NuScenesDataset(DefaultDataset): def __init__(self, sweeps=10, ignore_index=-1, **kwargs): self.sweeps = sweeps self.ignore_index = ignore_index self.learning_map = self.get_learning_map(ignore_index) super().__init__(ignore_index=ignore_index, **kwargs) def get_info_path(self, split): assert split in ["train", "val", "test"] if split == "train": return os.path.join( self.data_root, "info", f"nuscenes_infos_{self.sweeps}sweeps_train.pkl" ) elif split == "val": return os.path.join( self.data_root, "info", f"nuscenes_infos_{self.sweeps}sweeps_val.pkl" ) elif split == "test": return os.path.join( self.data_root, "info", f"nuscenes_infos_{self.sweeps}sweeps_test.pkl" ) else: raise NotImplementedError def get_data_list(self): if isinstance(self.split, str): info_paths = [self.get_info_path(self.split)] elif isinstance(self.split, Sequence): info_paths = [self.get_info_path(s) for s in self.split] else: raise NotImplementedError data_list = [] for info_path in info_paths: with open(info_path, "rb") as f: info = pickle.load(f) data_list.extend(info) return data_list def get_data(self, idx): data = self.data_list[idx % len(self.data_list)] lidar_path = os.path.join(self.data_root, "raw", data["lidar_path"]) points = np.fromfile(str(lidar_path), dtype=np.float32, count=-1).reshape( [-1, 5] ) coord = points[:, :3] strength = points[:, 3].reshape([-1, 1]) / 255 # scale strength to [0, 1] if "gt_segment_path" in data.keys(): gt_segment_path = os.path.join( self.data_root, "raw", data["gt_segment_path"] ) segment = np.fromfile( str(gt_segment_path), dtype=np.uint8, count=-1 ).reshape([-1]) segment = np.vectorize(self.learning_map.__getitem__)(segment).astype( np.int64 ) else: segment = np.ones((points.shape[0],), dtype=np.int64) * self.ignore_index data_dict = dict( coord=coord, strength=strength, segment=segment, name=self.get_data_name(idx), ) return data_dict def get_data_name(self, idx): # return data name for lidar seg, optimize the code when need to support detection return self.data_list[idx % len(self.data_list)]["lidar_token"] @staticmethod def get_learning_map(ignore_index): learning_map = { 0: ignore_index, 1: ignore_index, 2: 6, 3: 6, 4: 6, 5: ignore_index, 6: 6, 7: ignore_index, 8: ignore_index, 9: 0, 10: ignore_index, 11: ignore_index, 12: 7, 13: ignore_index, 14: 1, 15: 2, 16: 2, 17: 3, 18: 4, 19: ignore_index, 20: ignore_index, 21: 5, 22: 8, 23: 9, 24: 10, 25: 11, 26: 12, 27: 13, 28: 14, 29: ignore_index, 30: 15, 31: ignore_index, } return learning_map @DATASETS.register_module() class NuScenesColorNormalDataset(NuScenesDataset): @staticmethod def estimate_normals(points, center=np.array([0, 0, 0])): normals = points - center[None, :] norms = np.linalg.norm(normals, axis=1, keepdims=True) normals = normals / norms return normals def get_data(self, idx): data = self.data_list[idx % len(self.data_list)] lidar_path = os.path.join(self.data_root, "raw", data["lidar_path"]) points = np.fromfile(str(lidar_path), dtype=np.float32, count=-1).reshape( [-1, 5] ) coord = points[:, :3] normal = self.estimate_normals(coord) if "gt_segment_path" in data.keys(): gt_segment_path = os.path.join( self.data_root, "raw", data["gt_segment_path"] ) segment = np.fromfile( str(gt_segment_path), dtype=np.uint8, count=-1 ).reshape([-1]) segment = np.vectorize(self.learning_map.__getitem__)(segment).astype( np.int64 ) else: segment = np.ones((points.shape[0],), dtype=np.int64) * self.ignore_index data_dict = dict( coord=coord, color=np.zeros_like(coord), # placeholder for color normal=np.zeros_like(coord), # placeholder for normal segment=segment, name=self.get_data_name(idx), ) return data_dict @DATASETS.register_module() class NuScenesImagePointDataset(DefaultImagePointDataset): CAMERA_TYPES = [ "CAM_FRONT", "CAM_FRONT_RIGHT", "CAM_FRONT_LEFT", "CAM_BACK", "CAM_BACK_LEFT", "CAM_BACK_RIGHT", ] def __init__( self, if_img=False, if_sweep=False, sweeps_max=10, sweeps=10, sweep_gap=1, ignore_index=-1, img_num=4, **kwargs, ): self.sweeps = sweeps self.sweep_gap = sweep_gap self.sweeps_max = sweeps_max self.if_sweep = if_sweep self.if_img = if_img self.ignore_index = ignore_index self.learning_map = self.get_learning_map(ignore_index) frame_count = sweeps + 1 if if_sweep else 1 self.img_ratio = img_num / (6 * frame_count) super().__init__(ignore_index=ignore_index, if_img=if_img, **kwargs) @staticmethod def project_lidar_to_image_with_color( lidar_points, # shape: (N, 3) or (N, 4) image, # shape: (H, W, 3), uint8 RGB cam_intrinsic, # shape: (3, 3) lidar_to_cam, # shape: (4, 4) lidar_colors, ): """ Projects LiDAR points to the image, fetches pixel color and pixel coordinates. Returns: filtered_points: (M, 3) - 3D points in camera frame that project onto the image. colors: (M, 3) - RGB colors at projected 2D locations. uv_coords: (M, 2) - Integer pixel coordinates (u, v) on the image. mask: (N,) - (optional) Boolean mask indicating which lidar points are used. """ lidar_uv_coords = np.full( (lidar_points.shape[0], 2), -1, dtype=int ) # Default to (-1, -1) lidar_points_coord = lidar_points[:, :3] ones = np.ones((lidar_points_coord.shape[0], 1)) lidar_hom = np.concatenate([lidar_points_coord, ones], axis=1) # (N, 4) points_cam = (lidar_to_cam @ lidar_hom.T).T # (N, 4) valid = points_cam[:, 2] > 0 points_cam = points_cam[valid] pts_2d = (cam_intrinsic @ points_cam[:, :3].T).T # (N, 3) pts_2d = pts_2d[:, :2] / pts_2d[:, 2:3] # (N, 2) - pixel (u, v) H, W = image.shape[:2] u, v = pts_2d[:, 0], pts_2d[:, 1] inside = (u >= 0) & (u < W) & (v >= 0) & (v < H) u = u[inside].astype(int) v = v[inside].astype(int) mask = np.zeros(lidar_points.shape[0], dtype=bool) mask[np.where(valid)[0][inside]] = True lidar_colors[mask] = image[v, u, :] lidar_uv_coords[mask] = np.stack([u, v], axis=1) # (M, 2) return lidar_colors, lidar_uv_coords, mask @staticmethod def transform_normal(normal, transform): return (transform[:3, :3] @ normal.T).T def get_info_path(self, split): assert split in ["train", "val", "test"] if split == "train": return os.path.join( self.data_root, "info", f"nuscenes_infos_{self.sweeps_max}sweeps_train.pkl", ) elif split == "val": return os.path.join( self.data_root, "info", f"nuscenes_infos_{self.sweeps_max}sweeps_val.pkl", ) elif split == "test": return os.path.join( self.data_root, "info", f"nuscenes_infos_{self.sweeps_max}sweeps_test.pkl", ) else: raise NotImplementedError def get_data_list(self): split_list = {} if isinstance(self.split, str): info_paths = [self.get_info_path(self.split)] split = [self.split] elif isinstance(self.split, Sequence): split = self.split info_paths = [self.get_info_path(s) for s in self.split] else: raise NotImplementedError data_list = [] for info_path, split_i in zip(info_paths, split): with open(info_path, "rb") as f: info = pickle.load(f) data_list.extend(info) split_list[split_i] = list([i["token"] for i in info]) return data_list, split_list def get_data(self, idx): data = self.data_list[idx % len(self.data_list)] lidar_path = os.path.join(self.data_root, "raw", data["lidar_path"]) points = np.fromfile(str(lidar_path), dtype=np.float32, count=-1).reshape( [-1, 5] ) keyframe_point_count = points.shape[0] imgs = [] cam_coords = [] cam_normals = [] cam_colors = [] cam_strengths = [] cam_correspondences = [] correspondence_start = 0 frame_pcd_offset = [] lidar_colors = np.zeros((points.shape[0], 3), dtype=int) # Default to black for id, cam_name in enumerate(self.CAMERA_TYPES): cam_info = data["cams"][cam_name] cam_intrinsic = cam_info["camera_intrinsics"] cam_image = Image.open( os.path.join(self.data_root, "raw", data["cams"][cam_name]["data_path"]) ) cam_image_np = np.array(cam_image) sensor2lidar = np.eye(4) sensor2lidar[:3, :3] = cam_info["sensor2lidar_rotation"] sensor2lidar[:3, 3] = cam_info["sensor2lidar_translation"] lidar2sensor = np.linalg.inv(sensor2lidar) lidar_colors, correspondence_info, _ = ( self.project_lidar_to_image_with_color( points, cam_image_np, cam_intrinsic, lidar2sensor, lidar_colors ) ) correspondence_point_id = ( np.array(range(correspondence_info.shape[0])).reshape((-1, 1)) + correspondence_start ) correspondence_info = np.hstack( [correspondence_info, correspondence_point_id] ) if np.random.rand() < self.img_ratio: cam_correspondences.append(correspondence_info) imgs.append(cam_image) correspondence_start += points.shape[0] cam_coord = points[:, :3] cam_center = np.array([0, 0, 0]) cam_normal = self.get_normals(cam_center, cam_coord) cam_normals.append(cam_normal) cam_strength = points[:, 3].reshape([-1, 1]) / 255 cam_coords.append(cam_coord) cam_colors.append(lidar_colors) cam_strengths.append(cam_strength) if self.if_sweep: frame_pcd_offset.append(points.shape[0]) for id, sweep in enumerate( data["sweeps"][: (self.sweep_gap * self.sweeps) : self.sweep_gap] ): lidar_path = os.path.join(self.data_root, "raw", sweep["lidar_path"]) points = np.fromfile( str(lidar_path), dtype=np.float32, count=-1 ).reshape([-1, 5]) lidar_colors = np.zeros( (points.shape[0], 3), dtype=int ) # Default to black cam_lidar_tm = ( sweep["transform_matrix"] if sweep["transform_matrix"] is not None else np.eye(4) ) for id, cam_name in enumerate(self.CAMERA_TYPES): cam_info = sweep["cams"][cam_name] # cam_image_np = np.array(imgs[id]) cam_intrinsic = cam_info["camera_intrinsics"] cam_image = Image.open( os.path.join(self.data_root, "raw", cam_info["data_path"]) ) cam_image_np = np.array(cam_image) sensor2lidar = np.eye(4) sensor2lidar[:3, :3] = cam_info["sensor2lidar_rotation"] sensor2lidar[:3, 3] = cam_info["sensor2lidar_translation"] lidar2sensor = np.linalg.inv(sensor2lidar) @ cam_lidar_tm lidar_colors, correspondence_info, _ = ( self.project_lidar_to_image_with_color( points, cam_image_np, cam_intrinsic, lidar2sensor, lidar_colors, ) ) correspondence_point_id = ( np.array(range(correspondence_info.shape[0])).reshape((-1, 1)) + correspondence_start ) correspondence_info = np.hstack( [correspondence_info, correspondence_point_id] ) if np.random.rand() < self.img_ratio: cam_correspondences.append(correspondence_info) imgs.append(cam_image) correspondence_start += correspondence_info.shape[0] frame_pcd_offset.append(correspondence_start) cam_coord = points[:, :3] cam_center = np.array([0, 0, 0]) cam_normal = self.get_normals(cam_center, cam_coord) cam_normal = self.transform_normal(cam_normal, cam_lidar_tm) cam_normals.append(cam_normal) ones = np.ones((points.shape[0], 1)) cam_coord_hom = np.concatenate([cam_coord, ones], axis=1) # (N, 4) cam_coord = cam_coord_hom @ cam_lidar_tm.T cam_coord = cam_coord[:, :3] cam_strength = points[:, 3].reshape([-1, 1]) / 255 cam_coords.append(cam_coord) cam_colors.append(lidar_colors) cam_strengths.append(cam_strength) coord = np.vstack(cam_coords) color = np.vstack(cam_colors) normal = np.vstack(cam_normals) strength = np.vstack(cam_strengths) frame_pcd_offset = np.array(frame_pcd_offset) car_from_ref = np.linalg.inv(data["ref_from_car"]) coord_homo = np.hstack((coord, np.ones((coord.shape[0], 1)))) coord_homo = coord_homo @ car_from_ref.T coord = coord_homo[:, :3] normal = self.transform_normal(normal, car_from_ref) img_assets = dict() if self.if_img: if len(imgs) > 0: img_width, img_height = imgs[0].size div_w = img_width // self.patch_w div_h = img_height // self.patch_h div_min = max(min(div_w, div_h), 1) crop_img_width = div_min * self.patch_w crop_img_height = div_min * self.patch_h left = int((img_width - crop_img_width) / 2) top = int((img_height - crop_img_height) / 2) right = int((img_width + crop_img_width) / 2) bottom = int((img_height + crop_img_height) / 2) imgs = [img.crop((left, top, right, bottom)) for img in imgs] imgs = [self.transform_img(img) for img in imgs] imgs_list = torch.stack(imgs) img_assets["images"] = imgs_list.float() else: img_assets["images"] = torch.empty( ( 0, 3, self.patch_h * self.patch_size, self.patch_w * self.patch_size, ) ) img_assets["img_num"] = np.array( [img_assets["images"].shape[0]], dtype=np.int32 ) correspondence_infos = np.ones( (coord.shape[0], len(cam_correspondences), 2), dtype=np.int32 ) * (-1) for id, correspondence_info in enumerate(cam_correspondences): correspondence_info = self.resize_correspondence_info( correspondence_info, (self.patch_h * self.patch_size, self.patch_w * self.patch_size), (img_height, img_width), (left, top, right, bottom), self.patch_size, ) correspondence_infos[correspondence_info[:, -1], id, :] = ( correspondence_info[:, :-1] ) img_assets["correspondence"] = correspondence_infos if "gt_segment_path" in data.keys(): gt_segment_path = os.path.join( self.data_root, "raw", data["gt_segment_path"] ) segment = np.fromfile( str(gt_segment_path), dtype=np.uint8, count=-1 ).reshape([-1]) segment = np.vectorize(self.learning_map.__getitem__)(segment).astype( np.int64 ) else: segment = ( np.ones((keyframe_point_count,), dtype=np.int64) * self.ignore_index ) if segment.shape[0] != keyframe_point_count: raise ValueError( f"nuScenes sample {self.get_data_name(idx)} has " f"{keyframe_point_count} keyframe points but " f"{segment.shape[0]} segment labels." ) if self.if_sweep: sweep_point_count = coord.shape[0] - keyframe_point_count sweep_segment = np.full( (sweep_point_count,), self.ignore_index, dtype=segment.dtype ) segment = np.concatenate((segment, sweep_segment), axis=0) color = color.astype(np.float32) if self.if_sweep: data_dict = dict( coord=coord, color=color, normal=normal, strength=strength, segment=segment, frame_pcd_offset=frame_pcd_offset, name=self.get_data_name(idx), ) else: data_dict = dict( coord=coord, color=color, normal=normal, strength=strength, segment=segment, name=self.get_data_name(idx), ) data_dict.update(img_assets) return data_dict def get_data_name(self, idx): return self.data_list[idx % len(self.data_list)]["lidar_token"] @staticmethod def get_normals(cam_center, coords): Cs = np.repeat(cam_center.reshape((1, -1)), coords.shape[0], axis=0) view_dirs = coords - Cs view_dirs = view_dirs / np.linalg.norm(view_dirs, axis=-1, keepdims=True) pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(coords) pcd.estimate_normals() normals = np.asarray(pcd.normals) dot_product = np.sum(normals * view_dirs, axis=-1) flip_mask = dot_product > 0 normals[flip_mask] = -normals[flip_mask] normals = normals / np.linalg.norm(normals, axis=-1, keepdims=True) return normals # or using the vector from LiDAR to points, which is faster for data loading # @staticmethod # def get_normals(center, points): # normals = points - center[None, :] # norms = np.linalg.norm(normals, axis=1, keepdims=True) # normals = normals / norms # return normals @staticmethod def get_learning_map(ignore_index): learning_map = { 0: ignore_index, 1: ignore_index, 2: 6, 3: 6, 4: 6, 5: ignore_index, 6: 6, 7: ignore_index, 8: ignore_index, 9: 0, 10: ignore_index, 11: ignore_index, 12: 7, 13: ignore_index, 14: 1, 15: 2, 16: 2, 17: 3, 18: 4, 19: ignore_index, 20: ignore_index, 21: 5, 22: 8, 23: 9, 24: 10, 25: 11, 26: 12, 27: 13, 28: 14, 29: ignore_index, 30: 15, 31: ignore_index, } return learning_map