| import os |
| import sys |
| import glob |
| import torch |
| import shutil |
| import logging |
| from zipfile import ZipFile |
| from plyfile import PlyData |
| from typing import List |
| from torch_geometric.data.extract import extract_zip |
| from torch_geometric.nn.pool.consecutive import consecutive_cluster |
|
|
| from src.datasets import BaseDataset |
| from src.data import Data, InstanceData |
| from src.datasets.kitti360_config import * |
| from src.utils.neighbors import knn_2 |
| from src.utils.color import to_float_rgb |
|
|
|
|
| DIR = os.path.dirname(os.path.realpath(__file__)) |
| log = logging.getLogger(__name__) |
|
|
|
|
| |
| |
| |
| import torch.multiprocessing |
| torch.multiprocessing.set_sharing_strategy('file_system') |
|
|
|
|
| __all__ = ['KITTI360', 'MiniKITTI360'] |
|
|
|
|
| |
| |
| |
|
|
| def read_kitti360_window( |
| filepath: str, |
| xyz: bool = True, |
| rgb: bool = True, |
| semantic: bool = True, |
| instance: bool = True, |
| remap: bool = False |
| ) -> Data: |
| """Read a KITTI-360 window βi.e. a tileβ saved as PLY. |
| |
| :param filepath: str |
| Absolute path to the PLY file |
| :param xyz: bool |
| Whether XYZ coordinates should be saved in the output Data.pos |
| :param rgb: bool |
| Whether RGB colors should be saved in the output Data.rgb |
| :param semantic: bool |
| Whether semantic labels should be saved in the output Data.y |
| :param instance: bool |
| Whether instance labels should be saved in the output Data.obj |
| :param remap: bool |
| Whether semantic labels should be mapped from their KITTI-360 ID |
| to their train ID. For more details, see: |
| https://github.com/autonomousvision/kitti360Scripts/blob/master/kitti360scripts/evaluation/semantic_3d/evalPointLevelSemanticLabeling.py |
| """ |
| data = Data() |
| with open(filepath, "rb") as f: |
| window = PlyData.read(f) |
| attributes = [p.name for p in window['vertex'].properties] |
|
|
| if xyz: |
| pos = torch.stack([ |
| torch.FloatTensor(window["vertex"][axis]) |
| for axis in ["x", "y", "z"]], dim=-1) |
| pos_offset = pos[0] |
| data.pos = pos - pos_offset |
| data.pos_offset = pos_offset |
|
|
| if rgb: |
| data.rgb = to_float_rgb(torch.stack([ |
| torch.FloatTensor(window["vertex"][axis]) |
| for axis in ["red", "green", "blue"]], dim=-1)) |
|
|
| if semantic and 'semantic' in attributes: |
| y = torch.LongTensor(window["vertex"]['semantic']) |
| data.y = torch.from_numpy(ID2TRAINID)[y] if remap else y |
|
|
| if instance and 'instance' in attributes: |
| idx = torch.arange(data.num_points) |
| obj = torch.LongTensor(window["vertex"]['instance']) |
| |
| |
| obj = consecutive_cluster(obj)[0] |
| count = torch.ones_like(obj) |
| y = torch.LongTensor(window["vertex"]['semantic']) |
| y = torch.from_numpy(ID2TRAINID)[y] if remap else y |
| data.obj = InstanceData(idx, obj, count, y, dense=True) |
|
|
| return data |
|
|
|
|
| |
| |
| |
|
|
| class KITTI360(BaseDataset): |
| """KITTI360 dataset. |
| |
| Dataset website: http://www.cvlibs.net/datasets/kitti-360/ |
| |
| Parameters |
| ---------- |
| root : `str` |
| Root directory where the dataset should be saved. |
| stage : {'train', 'val', 'test', 'trainval'} |
| transform : `callable` |
| transform function operating on data. |
| pre_transform : `callable` |
| pre_transform function operating on data. |
| pre_filter : `callable` |
| pre_filter function operating on data. |
| on_device_transform: `callable` |
| on_device_transform function operating on data, in the |
| 'on_after_batch_transfer' hook. This is where GPU-based |
| augmentations should be, as well as any Transform you do not |
| want to run in CPU-based DataLoaders |
| """ |
|
|
| _form_url = CVLIBS_URL |
| _trainval_zip_name = DATA_3D_SEMANTICS_ZIP_NAME |
| _test_zip_name = DATA_3D_SEMANTICS_TEST_ZIP_NAME |
| _unzip_name = UNZIP_NAME |
|
|
| @property |
| def class_names(self) -> List[str]: |
| """List of string names for dataset classes. This list must be |
| one-item larger than `self.num_classes`, with the last label |
| corresponding to 'void', 'unlabelled', 'ignored' classes, |
| indicated as `y=self.num_classes` in the dataset labels. |
| """ |
| return CLASS_NAMES |
|
|
| @property |
| def num_classes(self) -> int: |
| """Number of classes in the dataset. Must be one-item smaller |
| than `self.class_names`, to account for the last class name |
| being used for 'void', 'unlabelled', 'ignored' classes, |
| indicated as `y=self.num_classes` in the dataset labels. |
| """ |
| return KITTI360_NUM_CLASSES |
|
|
| @property |
| def stuff_classes(self) -> List[int]: |
| """List of 'stuff' labels for INSTANCE and PANOPTIC |
| SEGMENTATION (setting this is NOT REQUIRED FOR SEMANTIC |
| SEGMENTATION alone). By definition, 'stuff' labels are labels in |
| `[0, self.num_classes-1]` which are not 'thing' labels. |
| |
| In instance segmentation, 'stuff' classes are not taken into |
| account in performance metrics computation. |
| |
| In panoptic segmentation, 'stuff' classes are taken into account |
| in performance metrics computation. Besides, each cloud/scene |
| can only have at most one instance of each 'stuff' class. |
| |
| IMPORTANT: |
| By convention, we assume `y β [0, self.num_classes-1]` ARE ALL |
| VALID LABELS (i.e. not 'ignored', 'void', 'unknown', etc), while |
| `y < 0` AND `y >= self.num_classes` ARE VOID LABELS. |
| """ |
| return STUFF_CLASSES |
|
|
| @property |
| def class_colors(self) -> List[List[int]]: |
| """Colors for visualization, if not None, must have the same |
| length as `self.num_classes`. If None, the visualizer will use |
| the label values in the data to generate random colors. |
| """ |
| return CLASS_COLORS |
|
|
| @property |
| def all_base_cloud_ids(self) -> List[str]: |
| """Dictionary holding lists of paths to the clouds, for each |
| stage. |
| |
| The following structure is expected: |
| `{'train': [...], 'val': [...], 'test': [...]}` |
| """ |
| return WINDOWS |
|
|
| def download_dataset(self) -> None: |
| """Download the KITTI-360 dataset. |
| """ |
| |
| zip_name = self._test_zip_name if self.stage == 'test' \ |
| else self._trainval_zip_name |
|
|
| |
| if not osp.exists(osp.join(self.root, zip_name)): |
| if self.stage != 'test': |
| msg = 'Accumulated Point Clouds for Train & Val (12G)' |
| else: |
| msg = 'Accumulated Point Clouds for Test (1.2G)' |
| log.error( |
| f"\nKITTI-360 does not support automatic download.\n" |
| f"Please go to the official webpage {self._form_url}, " |
| f"manually download the '{msg}' (i.e. '{zip_name}') to your " |
| f"'{self.root}/' directory, and re-run.\n" |
| f"The dataset will automatically be unzipped into the " |
| f"following structure:\n" |
| f"{self.raw_file_structure}\n") |
| sys.exit(1) |
|
|
| |
| |
| extract_zip(osp.join(self.root, zip_name), self.raw_dir) |
| stage = 'test' if self.stage == 'test' else 'train' |
| seqs = os.listdir(osp.join(self.raw_dir, 'data_3d_semantics', stage)) |
| for seq in seqs: |
| source = osp.join(self.raw_dir, 'data_3d_semantics', stage, seq) |
| target = osp.join(self.raw_dir, 'data_3d_semantics', seq) |
| shutil.move(source, target) |
| shutil.rmtree(osp.join(self.raw_dir, 'data_3d_semantics', stage)) |
|
|
| def read_single_raw_cloud(self, raw_cloud_path: str) -> 'Data': |
| """Read a single raw cloud and return a `Data` object, ready to |
| be passed to `self.pre_transform`. |
| |
| This `Data` object should contain the following attributes: |
| - `pos`: point coordinates |
| - `y`: OPTIONAL point semantic label |
| - `obj`: OPTIONAL `InstanceData` object with instance labels |
| - `rgb`: OPTIONAL point color |
| - `intensity`: OPTIONAL point LiDAR intensity |
| |
| IMPORTANT: |
| By convention, we assume `y β [0, self.num_classes-1]` ARE ALL |
| VALID LABELS (i.e. not 'ignored', 'void', 'unknown', etc), |
| while `y < 0` AND `y >= self.num_classes` ARE VOID LABELS. |
| This applies to both `Data.y` and `Data.obj.y`. |
| """ |
| return read_kitti360_window( |
| raw_cloud_path, semantic=True, instance=True, remap=True) |
|
|
| @property |
| def raw_file_structure(self) -> str: |
| return f""" |
| {self.root}/ |
| βββ raw/ |
| βββ data_3d_semantics/ |
| βββ 2013_05_28_drive_{{seq:0>4}}_sync/ |
| βββ static/ |
| βββ {{start_frame:0>10}}_{{end_frame:0>10}}.ply |
| """ |
|
|
| def id_to_relative_raw_path(self, id: str) -> str: |
| """Given a cloud id as stored in `self.cloud_ids`, return the |
| path (relative to `self.raw_dir`) of the corresponding raw |
| cloud. |
| """ |
| id = self.id_to_base_id(id) |
| return osp.join( |
| 'data_3d_semantics', id.split(os.sep)[0], 'static', |
| id.split(os.sep)[1] + '.ply') |
|
|
| def processed_to_raw_path(self, processed_path: str) -> str: |
| """Return the raw cloud path corresponding to the input |
| processed path. |
| """ |
| |
| stage, hash_dir, sequence_name, cloud_id = \ |
| osp.splitext(processed_path)[0].split(os.sep)[-4:] |
|
|
| |
| base_cloud_id = self.id_to_base_id(cloud_id) |
|
|
| |
| raw_path = osp.join( |
| self.raw_dir, 'data_3d_semantics', sequence_name, 'static', |
| base_cloud_id + '.ply') |
|
|
| return raw_path |
|
|
| def make_submission( |
| self, |
| idx: int, |
| pred: torch.Tensor, |
| pos: torch.Tensor, |
| submission_dir: str = None |
| ) -> None: |
| """Prepare data for a sumbission to KITTI360 for 3D semantic |
| Segmentation on the test set. |
| |
| Expected submission format is detailed here: |
| https://github.com/autonomousvision/kitti360Scripts/tree/master/kitti360scripts/evaluation/semantic_3d |
| """ |
| if self.xy_tiling or self.pc_tiling: |
| raise NotImplementedError( |
| f"Submission generation not implemented for tiled KITTI360 " |
| f"datasets yet...") |
|
|
| |
| if pred.dim() != 1: |
| raise ValueError( |
| f'The submission predictions must be 1D tensors, ' |
| f'received {type(pred)} of shape {pred.shape} instead.') |
|
|
| |
| |
| |
| |
| |
| submission_dir = submission_dir or self.submission_dir |
| if not osp.exists(submission_dir): |
| os.makedirs(submission_dir) |
|
|
| |
| raw_path = osp.join( |
| self.raw_dir, self.id_to_relative_raw_path(self.cloud_ids[idx])) |
| data_raw = self.sanitized_read_single_raw_cloud(raw_path) |
|
|
| |
| |
| neighbors = knn_2(pos, data_raw.pos, 1, r_max=1)[0] |
| pred_raw = pred[neighbors] |
|
|
| |
| pred_raw = np.asarray(pred_raw) |
| pred_remapped = TRAINID2ID[pred_raw].astype(np.uint8) |
|
|
| |
| |
| |
| sequence_name, window_name = self.id_to_base_id( |
| self.cloud_ids[idx]).split(os.sep) |
| seq = sequence_name.split('_')[-2] |
| start_frame, end_frame = window_name.split('_') |
| filename = f'{seq:0>4}_{start_frame:0>10}_{end_frame:0>10}.npy' |
|
|
| |
| np.save(osp.join(submission_dir, filename), pred_remapped) |
|
|
| def finalize_submission(self, submission_dir: str) -> None: |
| """This should be called once all window submission files have |
| been saved using `self._make_submission`. This will zip them |
| together as expected by the KITTI360 submission server. |
| """ |
| zipObj = ZipFile(f'{submission_dir}.zip', 'w') |
| for p in glob.glob(osp.join(submission_dir, '*.npy')): |
| zipObj.write(p) |
| zipObj.close() |
|
|
|
|
| |
| |
| |
|
|
| class MiniKITTI360(KITTI360): |
| """A mini version of KITTI360 with only a few windows for |
| experimentation. |
| """ |
| _NUM_MINI = 2 |
|
|
| @property |
| def all_cloud_ids(self) -> List[str]: |
| return {k: v[:self._NUM_MINI] for k, v in super().all_cloud_ids.items()} |
|
|
| @property |
| def data_subdir_name(self) -> str: |
| return self.__class__.__bases__[0].__name__.lower() |
|
|
| |
| |
| def process(self) -> None: |
| super().process() |
|
|
| |
| |
| def download(self) -> None: |
| super().download() |
|
|