From 8e01d88f1db4e0f7f12c6c389d68ca48b06b0196 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 23 Apr 2025 16:33:51 +0200 Subject: [PATCH 01/39] Move functions for reading LiDAR data to utils --- detectionmetrics/utils/lidar.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/detectionmetrics/utils/lidar.py b/detectionmetrics/utils/lidar.py index 21331782..a7699168 100644 --- a/detectionmetrics/utils/lidar.py +++ b/detectionmetrics/utils/lidar.py @@ -280,3 +280,29 @@ def render_point_cloud( renderer.scene.clear_geometry() return image + + +def read_semantickitti_points(fname: str) -> np.ndarray: + """Read points from a binary file in SemanticKITTI format + + :param fname: Binary file containing points + :type fname: str + :return: Numpy array containing points + :rtype: np.ndarray + """ + points = np.fromfile(fname, dtype=np.float32) + return points.reshape((-1, 4)) + +def read_semantickitti_label(fname: str) -> Tuple[np.ndarray, np.ndarray]: + """Read labels from a binary file in SemanticKITTI format + + :param fname: Binary file containing labels + :type fname: str + :return: Numpy arrays containing semantic and instance labels + :rtype: Tuple[np.ndarray, np.ndarray] + """ + label = np.fromfile(fname, dtype=np.uint32) + label = label.reshape((-1)) + semantic_label = label & 0xFFFF + instance_label = label >> 16 + return semantic_label, instance_label \ No newline at end of file From bcbf82a131f867ead54c3b97065ba8f4d6274bc4 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 23 Apr 2025 16:35:39 +0200 Subject: [PATCH 02/39] Update LiDAR segmentation dataset --- detectionmetrics/datasets/dataset.py | 72 +++++++++++++++++++--------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/detectionmetrics/datasets/dataset.py b/detectionmetrics/datasets/dataset.py index fc45796b..1b023544 100644 --- a/detectionmetrics/datasets/dataset.py +++ b/detectionmetrics/datasets/dataset.py @@ -11,6 +11,7 @@ import detectionmetrics.utils.io as uio import detectionmetrics.utils.conversion as uc +import detectionmetrics.utils.lidar as ul class SegmentationDataset(ABC): @@ -126,7 +127,7 @@ def export( outdir: str, new_ontology: Optional[dict] = None, ontology_translation: Optional[dict] = None, - ignored_classes: Optional[List[str]] = None, + classes_to_remove: Optional[List[str]] = None, resize: Optional[Tuple[int, int]] = None, include_label_count: bool = True, ): @@ -138,8 +139,8 @@ def export( :type new_ontology: dict :param ontology_translation: Ontology translation dictionary, defaults to None :type ontology_translation: Optional[dict], optional - :param ignored_classes: Classes to ignore from the old ontology, defaults to [] - :type ignored_classes: Optional[List[str]], optional + :param classes_to_remove: Classes to remove from the old ontology, defaults to [] + :type classes_to_remove: Optional[List[str]], optional :param resize: Resize images and labels to the given dimensions, defaults to None :type resize: Optional[Tuple[int, int]], optional :param include_label_count: Whether to include class weights in the dataset, defaults to True @@ -162,7 +163,8 @@ def export( old_ontology=self.ontology, new_ontology=new_ontology, ontology_translation=ontology_translation, - ignored_classes=ignored_classes, + classes_to_remove=classes_to_remove, + lut_dtype=np.uint32 ) n_classes = max(c["idx"] for c in new_ontology.values()) + 1 else: @@ -340,7 +342,8 @@ def export( outdir: str, new_ontology: Optional[dict] = None, ontology_translation: Optional[dict] = None, - ignored_classes: Optional[List[str]] = [], + classes_to_remove: Optional[List[str]] = [], + include_label_count: bool = True, ): """Export dataset dataframe and LiDAR files in SemanticKITTI format. Optionally, modify ontology before exporting. @@ -350,8 +353,10 @@ def export( :type new_ontology: dict :param ontology_translation: Ontology translation dictionary, defaults to None :type ontology_translation: Optional[dict], optional - :param ignored_classes: Classes to ignore from the old ontology, defaults to [] - :type ignored_classes: Optional[List[str]], optional + :param classes_to_remove: Classes to remove from the old ontology, defaults to [] + :type classes_to_remove: Optional[List[str]], optional + :param include_label_count: Whether to include class weights in the dataset, defaults to True + :type include_label_count: bool, optional """ os.makedirs(outdir, exist_ok=True) @@ -360,14 +365,25 @@ def export( if ontology_translation is not None and new_ontology is None: raise ValueError("New ontology must be provided") + # Create ontology conversion lookup table if needed and get number of classes ontology_conversion_lut = None if new_ontology is not None: ontology_conversion_lut = uc.get_ontology_conversion_lut( old_ontology=self.ontology, new_ontology=new_ontology, ontology_translation=ontology_translation, - ignored_classes=ignored_classes, + classes_to_remove=classes_to_remove, ) + n_classes = max(c["idx"] for c in new_ontology.values()) + 1 + else: + n_classes = max(c["idx"] for c in self.ontology.values()) + 1 + + # Check if label count is missing and create empty array if needed + label_count_missing = include_label_count and ( + not self.has_label_count or new_ontology is not None + ) + if label_count_missing: + label_count = np.zeros(n_classes, dtype=np.uint64) pbar = tqdm(self.dataset.iterrows()) @@ -392,13 +408,21 @@ def export( label_fname = os.path.join(self.dataset_dir, label_fname) # If format is not appropriate: read, convert, and rewrite sample - if not self.is_kitti_format or ontology_conversion_lut is not None: + if ( + not self.is_kitti_format + or ontology_conversion_lut is not None + or label_count_missing + ): points = self.read_points(points_fname) - label, _ = self.read_label(label_fname) + label = self.read_label(label_fname) if ontology_conversion_lut is not None: - label = ontology_conversion_lut[label] + label = ontology_conversion_lut[label].astype(np.uint32) points.tofile(os.path.join(outdir, rel_points_fname)) label.tofile(os.path.join(outdir, rel_label_fname)) + + if label_count_missing: + indices, counts = np.unique(label, return_counts=True) + label_count[indices] += counts.astype(np.uint64) else: shutil.copy2(points_fname, os.path.join(outdir, rel_points_fname)) shutil.copy2(label_fname, os.path.join(outdir, rel_label_fname)) @@ -406,9 +430,15 @@ def export( self.dataset.at[sample_name, "points"] = rel_points_fname self.dataset.at[sample_name, "label"] = rel_label_fname + # Update dataset directory and ontology if needed self.dataset_dir = outdir + self.ontology = new_ontology if new_ontology is not None else self.ontology # Write ontology and store relative path in dataset attributes + if label_count_missing: + for class_data in self.ontology.values(): + class_data["label_count"] = int(label_count[class_data["idx"]]) + ontology_fname = "ontology.json" self.dataset.attrs = {"ontology_fname": ontology_fname} uio.write_json(os.path.join(outdir, ontology_fname), self.ontology) @@ -418,27 +448,23 @@ def export( @staticmethod def read_points(fname: str) -> np.ndarray: - """Read points from a binary file in SemanticKITTI format + """Read point cloud. Defaults to SemanticKITTI format - :param fname: Binary file containing points + :param fname: File containing point cloud :type fname: str :return: Numpy array containing points :rtype: np.ndarray """ - points = np.fromfile(fname, dtype=np.float32) - return points.reshape((-1, 4)) + return ul.read_semantickitti_points(fname) @staticmethod def read_label(fname: str) -> Tuple[np.ndarray, np.ndarray]: - """Read labels from a binary file in SemanticKITTI format + """Read semantic labels. Defaults to SemanticKITTI format :param fname: Binary file containing labels :type fname: str - :return: Numpy arrays containing semantic and instance labels - :rtype: Tuple[np.ndarray, np.ndarray] + :return: Numpy arrays containing semantic labels + :rtype: np.ndarray """ - label = np.fromfile(fname, dtype=np.uint32) - label = label.reshape((-1)) - semantic_label = label & 0xFFFF - instance_label = label >> 16 - return semantic_label.astype(np.int32), instance_label.astype(np.int32) + label, _ = ul.read_semantickitti_label(fname) + return label From 36ff9fbf4886bd8963a37f36e50b3411d45ef562 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 23 Apr 2025 16:36:23 +0200 Subject: [PATCH 03/39] Move utils for torch-based models to separate module --- detectionmetrics/utils/torch.py | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 detectionmetrics/utils/torch.py diff --git a/detectionmetrics/utils/torch.py b/detectionmetrics/utils/torch.py new file mode 100644 index 00000000..ecb7a633 --- /dev/null +++ b/detectionmetrics/utils/torch.py @@ -0,0 +1,65 @@ +from typing import Union + +import torch + + +def data_to_device( + data: Union[tuple, list], device: torch.device +) -> Union[tuple, list]: + """Move provided data to given device (CPU or GPU) + + :param data: Data provided (it can be a single or multiple tensors) + :type data: Union[tuple, list] + :param device: Device to move data to + :type device: torch.device + :return: Data moved to device + :rtype: Union[tuple, list] + """ + if isinstance(data, (tuple, list)): + return type(data)( + d.to(device) if torch.is_tensor(d) else data_to_device(d, device) + for d in data + ) + elif torch.is_tensor(data): + return data.to(device) + else: + return data + + +def get_data_shape(data: Union[tuple, list]) -> Union[tuple, list]: + """Get the shape of the provided data + + :param data: Data provided (it can be a single or multiple tensors) + :type data: Union[tuple, list] + :return: Data shape + :rtype: Union[tuple, list] + """ + if isinstance(data, (tuple, list)): + return type(data)( + tuple(d.shape) if torch.is_tensor(d) else get_data_shape(d) for d in data + ) + elif torch.is_tensor(data): + return tuple(data.shape) + else: + return tuple(data.shape) + + +def unsqueeze_data(data: Union[tuple, list], dim: int = 0) -> Union[tuple, list]: + """Unsqueeze provided data along given dimension + + :param data: Data provided (it can be a single or multiple tensors) + :type data: Union[tuple, list] + :param dim: Dimension that will be unsqueezed, defaults to 0 + :type dim: int, optional + :return: Unsqueezed data + :rtype: Union[tuple, list] + """ + if isinstance(data, (tuple, list)): + return type(data)( + d.unsqueeze(dim) if torch.is_tensor(d) else unsqueeze_data(d, dim) + for d in data + ) + elif torch.is_tensor(data): + return data.unsqueeze(dim) + else: + return data From a2d40b0c98756945d04f5d2b616f74bb1c51782b Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 23 Apr 2025 16:38:23 +0200 Subject: [PATCH 04/39] Refactor torch model to support mmsegmentation-based models --- detectionmetrics/models/model.py | 1 - detectionmetrics/models/torch.py | 399 ++++++++---------- .../models/torch_model_utils/__init__.py | 44 +- .../torch_model_utils/mmdet3d/__init__.py | 50 +++ .../models/torch_model_utils/o3d/__init__.py | 111 +++++ .../{o3d_kpconv.py => o3d/kpconv.py} | 0 .../{o3d_randlanet.py => o3d/randlanet.py} | 2 +- 7 files changed, 345 insertions(+), 262 deletions(-) create mode 100644 detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py create mode 100644 detectionmetrics/models/torch_model_utils/o3d/__init__.py rename detectionmetrics/models/torch_model_utils/{o3d_kpconv.py => o3d/kpconv.py} (100%) rename detectionmetrics/models/torch_model_utils/{o3d_randlanet.py => o3d/randlanet.py} (99%) diff --git a/detectionmetrics/models/model.py b/detectionmetrics/models/model.py index 1ee3ecfd..45c21a4f 100644 --- a/detectionmetrics/models/model.py +++ b/detectionmetrics/models/model.py @@ -119,7 +119,6 @@ def get_lut_ontology( dataset_ontology, self.ontology, ontology_translation, - self.model_cfg.get("ignored_classes", []), ) return lut_ontology diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index 7182cba2..8b44673d 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -1,4 +1,3 @@ -from collections import defaultdict import os import time from typing import Any, List, Optional, Tuple, Union @@ -15,70 +14,59 @@ from detectionmetrics.datasets import dataset as dm_dataset from detectionmetrics.models import model as dm_model from detectionmetrics.models import torch_model_utils as tmu -import detectionmetrics.utils.lidar as ul import detectionmetrics.utils.metrics as um +import detectionmetrics.utils.lidar as ul +import detectionmetrics.utils.torch as ut -def data_to_device( - data: Union[tuple, list], device: torch.device -) -> Union[tuple, list]: - """Move provided data to given device (CPU or GPU) - - :param data: Data provided (it can be a single or multiple tensors) - :type data: Union[tuple, list] - :param device: Device to move data to - :type device: torch.device - :return: Data moved to device - :rtype: Union[tuple, list] - """ - if isinstance(data, (tuple, list)): - return type(data)( - d.to(device) if torch.is_tensor(d) else data_to_device(d, device) - for d in data - ) - elif torch.is_tensor(data): - return data.to(device) - else: - return data +AVAILABLE_INPUT_FORMATS_LIDAR = ["o3d_randlanet", "o3d_kpconv", "mmdet3d"] -def get_data_shape(data: Union[tuple, list]) -> Union[tuple, list]: - """Get the shape of the provided data +def raise_unknown_input_format_lidar(input_format: str) -> None: + """Raise an exception if the LiDAR model input format is unknown - :param data: Data provided (it can be a single or multiple tensors) - :type data: Union[tuple, list] - :return: Data shape - :rtype: Union[tuple, list] + :param input_format: Input format string + :type input_format: str """ - if isinstance(data, (tuple, list)): - return type(data)( - tuple(d.shape) if torch.is_tensor(d) else get_data_shape(d) for d in data - ) - elif torch.is_tensor(data): - return tuple(data.shape) - else: - return tuple(data.shape) - - -def unsqueeze_data(data: Union[tuple, list], dim: int = 0) -> Union[tuple, list]: - """Unsqueeze provided data along given dimension - - :param data: Data provided (it can be a single or multiple tensors) - :type data: Union[tuple, list] - :param dim: Dimension that will be unsqueezed, defaults to 0 - :type dim: int, optional - :return: Unsqueezed data - :rtype: Union[tuple, list] + msg = f"Unknown input format: {input_format}." + msg += f"Available formats: {AVAILABLE_INPUT_FORMATS_LIDAR}" + raise Exception(msg) + + +def get_mmdet3d_sample( + points_fname: str, + label_fname: Optional[str] = None, + name: Optional[str] = None, + idx: Optional[int] = None, + n_feats: int = 4, +) -> dict: + """Get sample data for mmdetection3d models + + :param points_fname: filename of the point cloud + :type points_fname: str + :param label_fname: filename of the semantic label, defaults to None + :type label_fname: Optional[str], optional + :param name: sample name, defaults to None + :type name: Optional[str], optional + :param idx: sample numerical index, defaults to None + :type idx: Optional[int], optional + :param n_feats: number of features, typically [x, y, z, r], defaults to 4 + :type n_feats: int, optional + :return: Sample data dictionary + :rtype: dict """ - if isinstance(data, (tuple, list)): - return type(data)( - d.unsqueeze(dim) if torch.is_tensor(d) else unsqueeze_data(d, dim) - for d in data - ) - elif torch.is_tensor(data): - return data.unsqueeze(dim) - else: - return data + + return { + "lidar_points": { + "lidar_path": points_fname, + "num_pts_feats": n_feats, + }, + "pts_semantic_mask_path": label_fname, + "sample_id": name, + "sample_idx": idx, + "num_pts_feats": n_feats, + "lidar_path": points_fname, + } def get_computational_cost( @@ -153,7 +141,7 @@ def get_computational_cost( inference_times.append(end_time - start_time) result = { - "input_shape": ["x".join(map(str, get_data_shape(dummy_input)))], + "input_shape": ["x".join(map(str, ut.get_data_shape(dummy_input)))], "n_params": [sum(p.numel() for p in model.parameters())], "size_mb": [size_mb], "inference_time_s": [np.mean(inference_times)], @@ -262,7 +250,7 @@ def __getitem__( class LiDARSegmentationTorchDataset(Dataset): - """Dataset for LiDAR segmentation PyTorch models + """Dataset for LiDAR segmentation PyTorch - Open3D-ML models :param dataset: LiDAR segmentation dataset :type dataset: LiDARSegmentationDataset @@ -270,8 +258,6 @@ class LiDARSegmentationTorchDataset(Dataset): :type model_cfg: dict :param preprocess: Function for preprocessing point clouds :type preprocess: callable - :param n_classes: Number of classes estimated by the model - :type n_classes: int :param splits: Splits to be used from the dataset, defaults to ["test"] :type splits: str, optional """ @@ -281,7 +267,6 @@ def __init__( dataset: dm_dataset.LiDARSegmentationDataset, model_cfg: dict, preprocess: callable, - n_classes: int, splits: str = ["test"], ): # Filter split and make filenames global @@ -291,7 +276,6 @@ def __init__( self.model_cfg = model_cfg self.preprocess = preprocess - self.n_classes = n_classes def __len__(self): return len(self.dataset.dataset) @@ -303,39 +287,81 @@ def __getitem__( :param idx: Sample index :type idx: int - :return: Point cloud and corresponding label tensor or numpy arrays - :rtype: Tuple[np.ndarray, np.ndarray,] + :return: Sample index, point cloud, projected indices, semantic label, and sampler + :rtype: Tuple[str, np.ndarray, np.ndarray, np.ndarray, ul.Sampler] """ # Read the point cloud and its labels points = self.dataset.read_points(self.dataset.dataset.iloc[idx]["points"]) - semantic_label, instance_label = self.dataset.read_label( + semantic_label = self.dataset.read_label( self.dataset.dataset.iloc[idx]["label"] ) # Preprocess point cloud - preprocessed_points, search_tree, projected_indices = self.preprocess( + preprocessed_points, projected_indices, sampler = self.preprocess( points, self.model_cfg ) - # Init sampler - sampler = None - if "sampler" in self.model_cfg: - sampler = ul.Sampler( - preprocessed_points.shape[0], - search_tree, - self.model_cfg["sampler"], - self.n_classes, - ) - return ( self.dataset.dataset.index[idx], preprocessed_points, projected_indices, - (semantic_label, instance_label), + semantic_label, sampler, ) +class LiDARSegmentationMMDet3DDataset(Dataset): + """Dataset for LiDAR segmentation PyTorch - mmdetection3d models + + :param dataset: LiDAR segmentation dataset + :type dataset: LiDARSegmentationDataset + :param model_cfg: Dictionary containing model configuration + :type model_cfg: dict + :param preprocess: Function for preprocessing point clouds + :type preprocess: callable + :param n_classes: Number of classes estimated by the model + :type n_classes: int + :param splits: Splits to be used from the dataset, defaults to ["test"] + :type splits: str, optional + """ + + def __init__( + self, + dataset: dm_dataset.LiDARSegmentationDataset, + model_cfg: dict, + preprocess: callable, + splits: str = ["test"], + ): + # Filter split and make filenames global + dataset.dataset = dataset.dataset[dataset.dataset["split"].isin(splits)] + self.dataset = dataset + self.dataset.make_fname_global() + + self.model_cfg = model_cfg + self.preprocess = preprocess + + def __len__(self): + return len(self.dataset.dataset) + + def __getitem__( + self, idx: int + ) -> Tuple[Union[np.ndarray, torch.Tensor], Union[np.ndarray, torch.Tensor]]: + """Prepare sample data: point cloud and label + + :param idx: Sample index + :type idx: int + :return: Point cloud and corresponding label tensor or numpy arrays + :rtype: Tuple[np.ndarray, np.ndarray,] + """ + sample = get_mmdet3d_sample( + points_fname=self.dataset.dataset.iloc[idx]["points"], + label_fname=self.dataset.dataset.iloc[idx]["label"], + name=self.dataset.dataset.index[idx], + idx=idx, + ) + return self.preprocess(sample) + + class TorchImageSegmentationModel(dm_model.ImageSegmentationModel): def __init__( @@ -661,6 +687,7 @@ def __init__( print("Model is not a TorchScript model. Loading as a PyTorch module.") model = torch.load(model, map_location=self.device) model_type = "native" + # Otherwise, check that it is a PyTorch module elif isinstance(model, torch.nn.Module): model_fname = None @@ -672,92 +699,61 @@ def __init__( super().__init__(model, model_type, model_cfg, ontology_fname, model_fname) self.model = self.model.to(self.device).eval() + # Init specific attributes and update model configuration + self.end_th = self.model_cfg.get("end_th", 0.5) + self.input_format = self.model_cfg["input_format"] + self.model_cfg["n_classes"] = self.n_classes + # Init model specific functions - if self.model_cfg["input_format"] == "o3d_randlanet": # Open3D RandLaNet - self.preprocess = tmu.preprocess - self.transform_input = tmu.o3d_randlanet.transform_input - self.update_probs = tmu.o3d_randlanet.update_probs - self.model_cfg["num_layers"] = sum(1 for _ in self.model.decoder.children()) - if self.model_cfg["input_format"] == "o3d_kpconv": # Open3D KPConv - self.preprocess = tmu.preprocess - self.transform_input = tmu.o3d_kpconv.transform_input - self.update_probs = tmu.o3d_kpconv.update_probs + if "o3d" in self.input_format: # Open3D-ML + self.preprocess = tmu.o3d.preprocess + self.transform_output = ( + lambda x: torch.argmax(x.squeeze(), axis=-1).squeeze().cpu().numpy() + ) + self._inference = tmu.o3d.inference + if self.input_format == "o3d_randlanet": # Open3D RandLaNet + self.transform_input = tmu.o3d.randlanet.transform_input + self.update_probs = tmu.o3d.randlanet.update_probs + decoder_layers = self.model.decoder.children() + self.model_cfg["num_layers"] = sum(1 for _ in decoder_layers) + elif self.input_format == "o3d_kpconv": # Open3D KPConv + self.transform_input = tmu.o3d.kpconv.transform_input + self.update_probs = tmu.o3d.kpconv.update_probs + else: + raise raise_unknown_input_format_lidar(self.input_format) + elif self.input_format == "mmdet3d": + self.preprocess = tmu.mmdet3d.preprocess + self._inference = tmu.mmdet3d.inference + self.transform_input = None + self.update_probs = None + self.transform_output = lambda x: x.cpu().numpy() else: - self.preprocess = tmu.preprocess - self.transform_input = tmu.transform_input - self.update_probs = tmu.update_probs - - # Transformation for output labels - self.transform_output = ( - lambda x: torch.argmax(x.squeeze(), axis=-1).squeeze().to(torch.uint8) - ) + raise raise_unknown_input_format_lidar(self.input_format) - def inference(self, points: np.ndarray) -> np.ndarray: + def inference(self, points_fname: str) -> np.ndarray: """Perform inference for a single point cloud - :param points: Point cloud xyz array - :type points: np.ndarray + :param points_fname: Point cloud in SemanticKITTI .bin format + :type points_fname: str :return: Segmenation result as a point cloud with label indices :rtype: np.ndarray """ # Preprocess point cloud - points, search_tree, projected_indices = self.preprocess(points, self.model_cfg) - - # Init sampler if needed - sampler = None - if "sampler" in self.model_cfg: - end_th = self.model_cfg.get("end_th", 0.5) - sampler = ul.Sampler( - points.shape[0], - search_tree, - self.model_cfg["sampler"], - self.n_classes, - ) - - # Iterate over the sampled point cloud until all points reach the end threshold. - # If no sampler is provided, the inference is performed in a single step. - infer_complete = False - while not infer_complete: - # Get model input data - input_data, selected_indices = self.transform_input( - points, self.model_cfg, sampler - ) - input_data = data_to_device(input_data, self.device) - if self.model_cfg["input_format"] != "o3d_kpconv": - input_data = unsqueeze_data(input_data) - # Perform inference - with torch.no_grad(): - result = self.model(*input_data) - - # TODO: check if this is consistent across different models - if isinstance(result, dict): - result = result["out"] - - # Update probabilities if sampler is used - if sampler is not None: - if self.model_cfg["input_format"] == "o3d_kpconv": - sampler.test_probs = self.update_probs( - result, - selected_indices, - sampler.test_probs, - lengths=input_data[-1], - ) - else: - sampler.test_probs = self.update_probs( - result, - selected_indices, - sampler.test_probs, - self.n_classes, - ) - if sampler.p[sampler.p > end_th].shape[0] == sampler.p.shape[0]: - result = sampler.test_probs[projected_indices] - infer_complete = True - else: - result = result.squeeze().cpu()[projected_indices].cuda() - infer_complete = True + if "o3d" in self.input_format: + points = ul.read_semantickitti_points(points_fname) + sample = self.preprocess(points, self.model_cfg) + points_fname, projected_indices, sampler = sample + pred = self._inference(points_fname, projected_indices, sampler, self) + elif self.input_format == "mmdet3d": + sample = get_mmdet3d_sample(points_fname=points_fname) + sample = self.preprocess(sample) + pred = self._inference(sample, self.model) + pred = pred.pred_pts_seg.pts_semantic_mask + else: + raise_unknown_input_format_lidar(self.input_format) - return self.transform_output(result).cpu().numpy() + return self.transform_output(pred) def eval( self, @@ -802,11 +798,15 @@ def eval( ignored_label_indices.append(dataset.ontology[ignored_class]["idx"]) # Get PyTorch dataset (no dataloader to avoid complexity with batching samplers) - dataset = LiDARSegmentationTorchDataset( + dataset_type = ( + LiDARSegmentationMMDet3DDataset + if self.input_format == "mmdet3d" + else LiDARSegmentationTorchDataset + ) + dataset = dataset_type( dataset, model_cfg=self.model_cfg, preprocess=self.preprocess, - n_classes=self.n_classes, splits=[split] if isinstance(split, str) else split, ) @@ -814,54 +814,29 @@ def eval( metrics_factory = um.MetricsFactory(self.n_classes) # Evaluation loop - end_th = self.model_cfg.get("end_th", 0.5) with torch.no_grad(): pbar = tqdm(dataset, total=len(dataset), leave=True) - for idx, points, projected_indices, (label, _), sampler in pbar: - # Iterate over the sampled point cloud until all points reach the end - # threshold. If no sampler is provided, the inference is performed in a - # single step. - infer_complete = False - while not infer_complete: - # Get model input data - input_data, selected_indices = self.transform_input( - points, self.model_cfg, sampler - ) - input_data = data_to_device(input_data, self.device) - if self.model_cfg["input_format"] != "o3d_kpconv": - input_data = unsqueeze_data(input_data) - - # Perform inference - pred = self.model(*input_data) - - # TODO: check if this is consistent across different models - if isinstance(pred, dict): - pred = pred["out"] - - if sampler is not None: - if self.model_cfg["input_format"] == "o3d_kpconv": - sampler.test_probs = self.update_probs( - pred, - selected_indices, - sampler.test_probs, - lengths=input_data[-1], - ) - else: - sampler.test_probs = self.update_probs( - pred, - selected_indices, - sampler.test_probs, - self.n_classes, - ) - if sampler.p[sampler.p > end_th].shape[0] == sampler.p.shape[0]: - pred = sampler.test_probs[projected_indices] - infer_complete = True - else: - pred = pred.squeeze().cpu()[projected_indices].cuda() - infer_complete = True + for sample in pbar: + # Perform inference + if "o3d" in self.input_format: + name, points, projected_indices, label, sampler = sample + label = torch.tensor(label, device=self.device) + pred = self._inference(points, projected_indices, sampler, self) + elif self.input_format == "mmdet3d": + pred_samples = self._inference(sample, self.model) + if not isinstance(pred_samples, list): + pred_samples = [pred_samples] + pred, label = [], [] + for pred_sample in pred_samples: + name = pred_sample.metainfo["sample_id"] + pred.append(pred_sample.pred_pts_seg.pts_semantic_mask) + label.append(pred_sample.gt_pts_seg.pts_semantic_mask) + pred = torch.stack(pred, dim=0) + label = torch.stack(label, dim=0) + else: + raise_unknown_input_format_lidar(self.input_format) # Get valid points masks depending on ignored label indices - label = torch.tensor(label, device=self.device) if ignored_label_indices: valid_mask = torch.ones_like(label, dtype=torch.bool) for idx in ignored_label_indices: @@ -875,7 +850,6 @@ def eval( # Prepare data and update metrics factory label = label.cpu().unsqueeze(0).numpy() - pred = self.transform_output(pred) pred = pred.cpu().unsqueeze(0).to(torch.int64).numpy() if valid_mask is not None: valid_mask = valid_mask.cpu().unsqueeze(0).numpy() @@ -884,8 +858,8 @@ def eval( # Store predictions and results per sample if required if predictions_outdir is not None: - for i, (sample_idx, sample_pred, sample_label) in enumerate( - zip(idx, pred, label) + for i, (sample_name, sample_pred, sample_label) in enumerate( + zip(name, pred, label) ): if results_per_sample: sample_valid_mask = ( @@ -899,10 +873,10 @@ def eval( sample_mf, self.ontology ) sample_df.to_csv( - os.path.join(predictions_outdir, f"{sample_idx}.csv") + os.path.join(predictions_outdir, f"{sample_name}.csv") ) pred.tofile( - os.path.join(predictions_outdir, f"{sample_idx}.bin") + os.path.join(predictions_outdir, f"{sample_name}.bin") ) return um.get_metrics_dataframe(metrics_factory, self.ontology) @@ -918,21 +892,12 @@ def get_computational_cost(self, runs: int = 30, warm_up_runs: int = 5) -> dict: """ # Build dummy input data (process is a bit complex for LiDAR models) dummy_points = np.random.rand(1000000, 4) - dummy_points, search_tree, _ = self.preprocess(dummy_points, self.model_cfg) - - sampler = None - if "sampler" in self.model_cfg: - sampler = ul.Sampler( - point_cloud_size=dummy_points.shape[0], - search_tree=search_tree, - sampler_name=self.model_cfg["sampler"], - num_classes=self.n_classes, - ) + dummy_points, _, sampler = self.preprocess(dummy_points, self.model_cfg) dummy_input, _ = self.transform_input(dummy_points, self.model_cfg, sampler) - dummy_input = data_to_device(dummy_input, self.device) - if self.model_cfg["input_format"] != "o3d_kpconv": - dummy_input = unsqueeze_data(dummy_input) + dummy_input = ut.data_to_device(dummy_input, self.device) + if self.input_format != "o3d_kpconv": + dummy_input = ut.unsqueeze_data(dummy_input) # Get computational cost return get_computational_cost( diff --git a/detectionmetrics/models/torch_model_utils/__init__.py b/detectionmetrics/models/torch_model_utils/__init__.py index 48f449a4..33210463 100644 --- a/detectionmetrics/models/torch_model_utils/__init__.py +++ b/detectionmetrics/models/torch_model_utils/__init__.py @@ -1,43 +1 @@ -from typing import Optional, Tuple - -import numpy as np - -try: - from open3d._ml3d.datasets.utils import DataProcessing -except Exception: - print("Open3D-ML3D not available") -from sklearn.neighbors import KDTree - -from detectionmetrics.models.torch_model_utils import o3d_randlanet, o3d_kpconv - - -# Default functions -def preprocess( - points: np.ndarray, cfg: Optional[dict] = {} -) -> Tuple[np.ndarray, KDTree, np.ndarray]: - """Preprocess point cloud data - - :param points: Point cloud data - :type points: np.ndarray - :param cfg: Dictionary containing model configuration, defaults to {} - :type cfg: Optional[dict], optional - :return: Subsampled points, search tree, and projected indices - :rtype: Tuple[np.ndarray, KDTree, np.ndarray] - """ - # Keep only XYZ coordinates - points = np.array(points[:, 0:3], dtype=np.float32) - - # Subsample points using a grid of given size - grid_size = cfg.get("grid_size", 0.06) - sub_points = DataProcessing.grid_subsampling(points, grid_size=grid_size) - - # Create search tree so that we can project points back to the original point cloud - search_tree = KDTree(sub_points) - projected_indices = np.squeeze(search_tree.query(points, return_distance=False)) - projected_indices = projected_indices.astype(np.int32) - - return sub_points, search_tree, projected_indices - - -transform_input = o3d_randlanet.transform_input -update_probs = o3d_randlanet.update_probs +from detectionmetrics.models.torch_model_utils import mmdet3d, o3d diff --git a/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py b/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py new file mode 100644 index 00000000..cc2efd17 --- /dev/null +++ b/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py @@ -0,0 +1,50 @@ +from mmdet3d.datasets.transforms import ( + LoadPointsFromFile, + LoadAnnotations3D, + Pack3DDetInputs, +) +from mmengine.registry import FUNCTIONS +from torchvision.transforms import Compose + +COLLATE_FN = FUNCTIONS.get("pseudo_collate") + + +def preprocess(sample): + n_feats = sample["num_pts_feats"] + transforms = [ + LoadPointsFromFile(coord_type="LIDAR", load_dim=n_feats, use_dim=n_feats) + ] + if sample["pts_semantic_mask_path"] is not None: + transforms.append( + LoadAnnotations3D( + with_bbox_3d=False, + with_label_3d=False, + with_seg_3d=True, + seg_3d_dtype="np.uint32", + seg_offset=65536, + dataset_type="semantickitti", + ) + ) + transforms.append( + Pack3DDetInputs( + keys=["points", "pts_semantic_mask"], + meta_keys=["sample_idx", "lidar_path", "num_pts_feats", "sample_id"], + ) + ) + transforms = Compose(transforms) + return transforms(sample) + + +def inference(sample, model): + single_sample = not isinstance(sample["data_samples"], list) + if single_sample: + sample = COLLATE_FN([sample]) + + sample = model.data_preprocessor(sample, training=False) + inputs, data_samples = sample["inputs"], sample["data_samples"] + pred = model(inputs, data_samples, mode="predict") + + if single_sample: + pred = pred[0] + + return pred diff --git a/detectionmetrics/models/torch_model_utils/o3d/__init__.py b/detectionmetrics/models/torch_model_utils/o3d/__init__.py new file mode 100644 index 00000000..c7a1aa5d --- /dev/null +++ b/detectionmetrics/models/torch_model_utils/o3d/__init__.py @@ -0,0 +1,111 @@ +from __future__ import annotations # used to avoid circular import in type annotation +from typing import Tuple + + +import numpy as np +import torch + +try: + from open3d._ml3d.datasets.utils import DataProcessing +except Exception: + print("Open3D-ML3D not available") +from sklearn.neighbors import KDTree + +from detectionmetrics.models.torch_model_utils.o3d import kpconv, randlanet +from detectionmetrics.utils import lidar as ul +import detectionmetrics.utils.torch as ut + + +def preprocess( + points: np.ndarray, cfg: dict +) -> Tuple[np.ndarray, np.ndarray, ul.Sampler]: + """Preprocess point cloud data + + :param points: Point cloud data + :type points: np.ndarray + :param cfg: Dictionary containing model configuration, defaults to {} + :type cfg: dict + :return: Subsampled points, projected indices and sampler + :rtype: Tuple[np.ndarray, np.ndarray, ul.Sampler] + """ + # Keep only XYZ coordinates + points = np.array(points[:, 0:3], dtype=np.float32) + + # Subsample points using a grid of given size + grid_size = cfg.get("grid_size", 0.06) + sub_points = DataProcessing.grid_subsampling(points, grid_size=grid_size) + + # Create search tree so that we can project points back to the original point cloud + search_tree = KDTree(sub_points) + projected_indices = np.squeeze(search_tree.query(points, return_distance=False)) + projected_indices = projected_indices.astype(np.int32) + + # Init sampler + sampler = None + if "sampler" in cfg: + sampler = ul.Sampler( + sub_points.shape[0], search_tree, cfg["sampler"], cfg["n_classes"] + ) + + return sub_points, projected_indices, sampler + + +def inference( + points: np.ndarray, + projected_indices: np.ndarray, + sampler: ul.Sampler, + model: TorchLiDARSegmentationModel, # type: ignore (future annotation) +) -> torch.Tensor: + """Perform inference on the point cloud data + :param points: Point cloud data + :type points: np.ndarray + :param projected_indices: Indices of the projected points + :type projected_indices: np.ndarray + :param sampler: Sampler object for sampling point cloud + :type sampler: ul.Sampler + :param model: Model object for inference + :type model: TorchLiDARSegmentationModel + :return: Inference result + :rtype: torch.Tensor + """ + infer_complete = False + while not infer_complete: + # Get model input data + input_data, selected_indices = model.transform_input( + points, model.model_cfg, sampler + ) + input_data = ut.data_to_device(input_data, model.device) + if model.input_format != "o3d_kpconv": + input_data = ut.unsqueeze_data(input_data) + + # Perform inference + with torch.no_grad(): + pred = model.model(*input_data) + + # TODO: check if this is consistent across different models + if isinstance(pred, dict): + pred = pred["out"] + + # Update probabilities if sampler is used + if sampler is not None: + if model.input_format == "o3d_kpconv": + sampler.test_probs = model.update_probs( + pred, + selected_indices, + sampler.test_probs, + lengths=input_data[-1], + ) + else: + sampler.test_probs = model.update_probs( + pred, + selected_indices, + sampler.test_probs, + model.n_classes, + ) + if sampler.p[sampler.p > model.end_th].shape[0] == sampler.p.shape[0]: + pred = sampler.test_probs[projected_indices] + infer_complete = True + else: + pred = pred.squeeze().cpu()[projected_indices].cuda() + infer_complete = True + return pred diff --git a/detectionmetrics/models/torch_model_utils/o3d_kpconv.py b/detectionmetrics/models/torch_model_utils/o3d/kpconv.py similarity index 100% rename from detectionmetrics/models/torch_model_utils/o3d_kpconv.py rename to detectionmetrics/models/torch_model_utils/o3d/kpconv.py diff --git a/detectionmetrics/models/torch_model_utils/o3d_randlanet.py b/detectionmetrics/models/torch_model_utils/o3d/randlanet.py similarity index 99% rename from detectionmetrics/models/torch_model_utils/o3d_randlanet.py rename to detectionmetrics/models/torch_model_utils/o3d/randlanet.py index 8caad287..1210b6a7 100644 --- a/detectionmetrics/models/torch_model_utils/o3d_randlanet.py +++ b/detectionmetrics/models/torch_model_utils/o3d/randlanet.py @@ -109,4 +109,4 @@ def update_probs( test_probs = torch.tensor(test_probs, device=new_probs.device) test_probs[indices] = weight * test_probs[indices] + (1 - weight) * new_probs - return test_probs + return test_probs \ No newline at end of file From 453366f99cc7f7869d9496f7008cdcb1fac53053 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 23 Apr 2025 16:38:52 +0200 Subject: [PATCH 05/39] Update ontology conversion LUT --- detectionmetrics/utils/conversion.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/detectionmetrics/utils/conversion.py b/detectionmetrics/utils/conversion.py index 9cf8e9ce..1518aec9 100644 --- a/detectionmetrics/utils/conversion.py +++ b/detectionmetrics/utils/conversion.py @@ -57,7 +57,8 @@ def get_ontology_conversion_lut( old_ontology: dict, new_ontology: dict, ontology_translation: Optional[dict] = None, - ignored_classes: Optional[List[str]] = None, + classes_to_remove: Optional[List[str]] = None, + lut_dtype: Optional[np.dtype] = np.uint8, ) -> np.ndarray: """Build a LUT that links old ontology and new ontology indices. If class names don't match between the provided ontologies, user must provide an ontology @@ -69,18 +70,20 @@ def get_ontology_conversion_lut( :type new_ontology: dict :param ontology_translation: Ontology translation dictionary, defaults to None :type ontology_translation: Optional[dict], optional - :param ignored_classes: Classes to ignore from the old ontology, defaults to None - :type ignored_classes: Optional[List[str]], optional + :param classes_to_remove: Classes to be removed from the old ontology, defaults to None + :type classes_to_remove: Optional[List[str]], optional + :param lut_dtype: Type for the ontology conversion LUT, defaults to np.uint8 + :type lut_dtype: Optional[np.dtype], optional :return: numpy array associating old and new ontology indices :rtype: np.ndarray """ - ignored_classes = [] if ignored_classes is None else ignored_classes + classes_to_remove = [] if classes_to_remove is None else classes_to_remove max_idx = max(class_data["idx"] for class_data in old_ontology.values()) - lut = np.zeros((max_idx + 1), dtype=np.uint8) + lut = np.zeros((max_idx + 1), dtype=lut_dtype) if ontology_translation is not None: - # Deleting ignored classes that exist in ontology_translation - for class_name in ignored_classes: + # Deleting requested classes from ontology translation + for class_name in classes_to_remove: if class_name in ontology_translation: del ontology_translation[class_name] @@ -91,7 +94,8 @@ def get_ontology_conversion_lut( lut[old_class_idx] = new_class_idx else: old_ontology = old_ontology.copy() - for class_name in ignored_classes: # Deleting ignored classes from old_ontology + # Deleting classes requested from old ontology + for class_name in classes_to_remove: del old_ontology[class_name] assert set(old_ontology.keys()) == set( # Checking ontology compatibility new_ontology.keys() From caab46212ca035a98a01792f3e59b749b181e803 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 23 Apr 2025 16:39:11 +0200 Subject: [PATCH 06/39] Downgrade packages for compatibility with mmsegmentation --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eaed32e7..f038f718 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ license = "LICENSE" [tool.poetry.dependencies] python = "^3.10" -tqdm = "^4.67.0" +tqdm = "^4.65.0" pandas = "^2.2.3" PyYAML = "^6.0.2" pyarrow = "^18.0.0" @@ -18,7 +18,7 @@ opencv-python-headless = "^4.10.0.84" scikit-learn = "^1.6.0" open3d = "^0.19.0" addict = "^2.4.0" -matplotlib = "^3.10.0" +matplotlib = "^3.6.0" click = "^8.1.8" tensorboard = "^2.18.0" From 5555a294d628811204c078d49c868b23d51b9987 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 23 Apr 2025 16:39:41 +0200 Subject: [PATCH 07/39] Update examples --- examples/rellis3d_lidar.py | 26 +++++++++++++++++++++++++- examples/store_lidar_video.py | 2 +- examples/torch_lidar.py | 4 ++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/examples/rellis3d_lidar.py b/examples/rellis3d_lidar.py index a5a1cc93..cb5bf48e 100644 --- a/examples/rellis3d_lidar.py +++ b/examples/rellis3d_lidar.py @@ -1,4 +1,5 @@ import argparse +import json from detectionmetrics.datasets.rellis3d import Rellis3DLiDARSegmentationDataset @@ -28,6 +29,16 @@ def parse_args() -> argparse.Namespace: required=True, help="YAML file containing dataset ontology", ) + parser.add_argument( + "--new_ontology", + type=str, + help="New ontology JSON file name", + ) + parser.add_argument( + "--ontology_translation", + type=str, + help="Ontology translation JSON file name", + ) parser.add_argument( "--outdir", type=str, @@ -42,12 +53,25 @@ def main(): """Main function""" args = parse_args() + new_ontology, ontology_translation = None, None + if args.new_ontology is not None: + with open(args.new_ontology, "r", encoding="utf-8") as f: + new_ontology = json.load(f) + + if args.ontology_translation is not None: + with open(args.ontology_translation, "r", encoding="utf-8") as f: + ontology_translation = json.load(f) + dataset = Rellis3DLiDARSegmentationDataset( dataset_dir=args.dataset_dir, split_dir=args.split_dir, ontology_fname=args.ontology_fname, ) - dataset.export(args.outdir) + dataset.export( + outdir=args.outdir, + new_ontology=new_ontology, + ontology_translation=ontology_translation, + ) if __name__ == "__main__": diff --git a/examples/store_lidar_video.py b/examples/store_lidar_video.py index f5569cad..c8ce31d2 100644 --- a/examples/store_lidar_video.py +++ b/examples/store_lidar_video.py @@ -115,7 +115,7 @@ def main(): label = model.inference(point_cloud) lut = uc.ontology_to_rgb_lut(model.ontology) else: - label, _ = dataset.read_label(sample_data["label"]) + label = dataset.read_label(sample_data["label"]) lut = uc.ontology_to_rgb_lut(dataset.ontology) colors = lut[label] / 255.0 diff --git a/examples/torch_lidar.py b/examples/torch_lidar.py index f8a24b28..65aa1085 100644 --- a/examples/torch_lidar.py +++ b/examples/torch_lidar.py @@ -75,10 +75,10 @@ def main(): dataset = GaiaLiDARSegmentationDataset(args.dataset) if args.point_cloud is not None: - point_cloud = dataset.read_points(args.point_cloud) - result = model.inference(point_cloud) + result = model.inference(args.point_cloud) lut = uc.ontology_to_rgb_lut(model.ontology) colors = lut[result] / 255.0 + point_cloud = dataset.read_points(args.point_cloud) ul.view_point_cloud(point_cloud[:, :3], colors) results = model.eval( From 729ae06ffef76d05999814263edd55e336f86375 Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Thu, 24 Apr 2025 10:38:05 +0200 Subject: [PATCH 08/39] Allow for 3 or 4 features in LiDAR segmentation --- detectionmetrics/models/torch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index 8b44673d..75c20ddb 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -358,6 +358,7 @@ def __getitem__( label_fname=self.dataset.dataset.iloc[idx]["label"], name=self.dataset.dataset.index[idx], idx=idx, + n_feats=self.model_cfg.get("n_feats", 4), ) return self.preprocess(sample) @@ -746,7 +747,9 @@ def inference(self, points_fname: str) -> np.ndarray: points_fname, projected_indices, sampler = sample pred = self._inference(points_fname, projected_indices, sampler, self) elif self.input_format == "mmdet3d": - sample = get_mmdet3d_sample(points_fname=points_fname) + sample = get_mmdet3d_sample( + points_fname=points_fname, n_feats=self.model_cfg.get("n_feats", 4) + ) sample = self.preprocess(sample) pred = self._inference(sample, self.model) pred = pred.pred_pts_seg.pts_semantic_mask From a09358899e3ac47adc067bcb4d02e332a96ecc73 Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Thu, 24 Apr 2025 10:38:27 +0200 Subject: [PATCH 09/39] Minor fix --- detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py b/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py index cc2efd17..b769cbb3 100644 --- a/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py +++ b/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py @@ -12,7 +12,7 @@ def preprocess(sample): n_feats = sample["num_pts_feats"] transforms = [ - LoadPointsFromFile(coord_type="LIDAR", load_dim=n_feats, use_dim=n_feats) + LoadPointsFromFile(coord_type="LIDAR", load_dim=4, use_dim=n_feats) ] if sample["pts_semantic_mask_path"] is not None: transforms.append( From e551d4288398e21e006fd2f6e82400588419532a Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Fri, 9 May 2025 16:10:06 +0200 Subject: [PATCH 10/39] Refactor lidar torch utils --- .../models/lidar_torch_utils/__init__.py | 1 + .../lsk3dnet.py} | 0 .../models/lidar_torch_utils/mmdet3d.py | 107 ++++++++ .../models/lidar_torch_utils/o3d/__init__.py | 142 +++++++++++ .../o3d/kpconv.py | 2 +- .../o3d/randlanet.py | 0 detectionmetrics/models/model.py | 54 ++-- detectionmetrics/models/torch.py | 232 +++--------------- .../models/torch_model_utils/__init__.py | 1 - .../models/torch_model_utils/o3d/__init__.py | 111 --------- 10 files changed, 327 insertions(+), 323 deletions(-) create mode 100644 detectionmetrics/models/lidar_torch_utils/__init__.py rename detectionmetrics/models/{torch_model_utils/mmdet3d/__init__.py => lidar_torch_utils/lsk3dnet.py} (100%) create mode 100644 detectionmetrics/models/lidar_torch_utils/mmdet3d.py create mode 100644 detectionmetrics/models/lidar_torch_utils/o3d/__init__.py rename detectionmetrics/models/{torch_model_utils => lidar_torch_utils}/o3d/kpconv.py (99%) rename detectionmetrics/models/{torch_model_utils => lidar_torch_utils}/o3d/randlanet.py (100%) delete mode 100644 detectionmetrics/models/torch_model_utils/__init__.py delete mode 100644 detectionmetrics/models/torch_model_utils/o3d/__init__.py diff --git a/detectionmetrics/models/lidar_torch_utils/__init__.py b/detectionmetrics/models/lidar_torch_utils/__init__.py new file mode 100644 index 00000000..5bb904a0 --- /dev/null +++ b/detectionmetrics/models/lidar_torch_utils/__init__.py @@ -0,0 +1 @@ +from detectionmetrics.models.lidar_torch_utils import o3d, mmdet3d \ No newline at end of file diff --git a/detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py similarity index 100% rename from detectionmetrics/models/torch_model_utils/mmdet3d/__init__.py rename to detectionmetrics/models/lidar_torch_utils/lsk3dnet.py diff --git a/detectionmetrics/models/lidar_torch_utils/mmdet3d.py b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py new file mode 100644 index 00000000..8fb0772d --- /dev/null +++ b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py @@ -0,0 +1,107 @@ +from typing import List, Optional, Tuple + +from mmdet3d.datasets.transforms import ( + LoadPointsFromFile, + LoadAnnotations3D, + Pack3DDetInputs, +) +from mmengine.registry import FUNCTIONS +import torch +from torchvision.transforms import Compose + +COLLATE_FN = FUNCTIONS.get("pseudo_collate") + + +def get_sample( + points_fname: str, + model_cfg: dict, + label_fname: Optional[str] = None, + name: Optional[str] = None, + idx: Optional[int] = None, +) -> dict: + """Get sample data for mmdetection3d models + + :param points_fname: filename of the point cloud + :type points_fname: str + :param model_cfg: model configuration + :type model_cfg: dict + :param label_fname: filename of the semantic label, defaults to None + :type label_fname: Optional[str], optional + :param name: sample name, defaults to None + :type name: Optional[str], optional + :param idx: sample numerical index, defaults to None + :type idx: Optional[int], optional + :return: Sample data dictionary + :rtype: dict + """ + sample = { + "lidar_points": { + "lidar_path": points_fname, + "num_pts_feats": model_cfg.get("n_feats", 4), + }, + "pts_semantic_mask_path": label_fname, + "sample_id": name, + "sample_idx": idx, + "num_pts_feats": model_cfg.get("n_feats", 4), + "lidar_path": points_fname, + } + + n_feats = sample["num_pts_feats"] + transforms = [LoadPointsFromFile(coord_type="LIDAR", load_dim=4, use_dim=n_feats)] + if sample["pts_semantic_mask_path"] is not None: + transforms.append( + LoadAnnotations3D( + with_bbox_3d=False, + with_label_3d=False, + with_seg_3d=True, + seg_3d_dtype="np.uint32", + seg_offset=65536, + dataset_type="semantickitti", + ) + ) + transforms.append( + Pack3DDetInputs( + keys=["points", "pts_semantic_mask"], + meta_keys=["sample_idx", "lidar_path", "num_pts_feats", "sample_id"], + ) + ) + transforms = Compose(transforms) + + return transforms(sample) + + +def inference( + sample: dict, model: torch.nn.Module, model_cfg: dict +) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: + """Perform inference on a sample using an mmdetection3D model + + :param sample: sample data dictionary + :type sample: dict + :param model: mmdetection3D model + :type model: torch.nn.Module + :param model_cfg: model configuration + :type model_cfg: dict + :return: predictions, labels, and sample names + :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + """ + single_sample = not isinstance(sample["data_samples"], list) + if single_sample: + sample = COLLATE_FN([sample]) + + sample = model.data_preprocessor(sample, training=False) + inputs, data_samples = sample["inputs"], sample["data_samples"] + has_labels = hasattr(data_samples[0].gt_pts_seg, "pts_semantic_mask") + + outputs = model(inputs, data_samples, mode="predict") + + preds, labels, names = ([], [], []) if has_labels else ([], None, None) + for output in outputs: + preds.append(output.pred_pts_seg.pts_semantic_mask) + if has_labels: + labels.append(output.gt_pts_seg.pts_semantic_mask) + names.append(output.metainfo["sample_id"]) + preds = torch.stack(preds, dim=0).squeeze() + if has_labels: + labels = torch.stack(labels, dim=0).squeeze() + + return preds, labels, names diff --git a/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py new file mode 100644 index 00000000..41df93e9 --- /dev/null +++ b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py @@ -0,0 +1,142 @@ +from typing import Optional, Tuple + +import numpy as np +import torch + +try: + from open3d._ml3d.datasets.utils import DataProcessing +except Exception: + print("Open3D-ML3D not available") +from sklearn.neighbors import KDTree + +from detectionmetrics.models.lidar_torch_utils.o3d import kpconv, randlanet +from detectionmetrics.utils import lidar as ul +import detectionmetrics.utils.torch as ut + + +def inference( + sample: Tuple[np.ndarray, np.ndarray, ul.Sampler], + model: torch.nn.Module, + model_cfg: dict, +) -> torch.Tensor: + """Perform inference on a sample using an Open3D-ML model + + :param sample: sample data dictionary + :type sample: dict + :param model: Open3D-ML model + :type model: torch.nn.Module + :param model_cfg: model configuration + :type model_cfg: dict + :return: predictions, labels, and sample names + :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + """ + infer_complete = False + points, projected_indices, sampler, label, name, _ = sample + model_format = model_cfg["model_format"] + end_th = model_cfg.get("end_th", 0.5) + + if "kpconv" in model_format: + transform_input = kpconv.transform_input + update_probs = kpconv.update_probs + elif "randlanet" in model_format: + decoder_layers = model.decoder.children() + model_cfg["num_layers"] = sum(1 for _ in decoder_layers) + transform_input = randlanet.transform_input + update_probs = randlanet.update_probs + else: + raise ValueError(f"Unknown model type: {model_format}") + + while not infer_complete: + # Get model input data + input_data, selected_indices = transform_input(points, model_cfg, sampler) + input_data = ut.data_to_device(input_data, model.device) + if "randlanet" in model_format: + input_data = ut.unsqueeze_data(input_data) + + # Perform inference + with torch.no_grad(): + pred = model(*input_data) + + # TODO: check if this is consistent across different models + if isinstance(pred, dict): + pred = pred["out"] + + # Update probabilities if sampler is used + if sampler is not None: + if "kpconv" in model_format: + sampler.test_probs = update_probs( + pred, + selected_indices, + sampler.test_probs, + lengths=input_data[-1], + ) + else: + sampler.test_probs = update_probs( + pred, + selected_indices, + sampler.test_probs, + model_cfg["n_classes"], + ) + if sampler.p[sampler.p > end_th].shape[0] == sampler.p.shape[0]: + pred = sampler.test_probs[projected_indices] + infer_complete = True + else: + pred = pred.squeeze().cpu()[projected_indices].cuda() + infer_complete = True + + if label is not None: + label = torch.from_numpy(label.astype(np.int64)).long().cuda() + + return torch.argmax(pred.squeeze(), axis=-1), label, name + + +def get_sample( + points_fname: str, + model_cfg: dict, + label_fname: Optional[str] = None, + name: Optional[str] = None, + idx: Optional[int] = None, +) -> Tuple[np.ndarray, np.ndarray, ul.Sampler]: + """Get sample data for mmdetection3d models + + :param points_fname: filename of the point cloud + :type points_fname: str + :param model_cfg: model configuration + :type model_cfg: dict + :param label_fname: filename of the semantic label, defaults to None + :type label_fname: Optional[str], optional + :param name: sample name, defaults to None + :type name: Optional[str], optional + :param idx: sample numerical index, defaults to None + :type idx: Optional[int], optional + :return: Sample data tuple + :rtype: Tuple[np.ndarray, np.ndarray, ul.Sampler, np.ndarray, str, int] + """ + points = ul.read_semantickitti_points(points_fname) + label = None + if label_fname is not None: + label, _ = ul.read_semantickitti_label(label_fname) + + # Keep only XYZ coordinates + points = np.array(points[:, 0:3], dtype=np.float32) + + # Subsample points using a grid of given size + grid_size = model_cfg.get("grid_size", 0.06) + sub_points = DataProcessing.grid_subsampling(points, grid_size=grid_size) + + # Create search tree so that we can project points back to the original point cloud + search_tree = KDTree(sub_points) + projected_indices = np.squeeze(search_tree.query(points, return_distance=False)) + projected_indices = projected_indices.astype(np.int32) + + # Init sampler + sampler = None + if "sampler" in model_cfg: + sampler = ul.Sampler( + sub_points.shape[0], + search_tree, + model_cfg["sampler"], + model_cfg["n_classes"], + ) + + return sub_points, projected_indices, sampler, label, name, idx diff --git a/detectionmetrics/models/torch_model_utils/o3d/kpconv.py b/detectionmetrics/models/lidar_torch_utils/o3d/kpconv.py similarity index 99% rename from detectionmetrics/models/torch_model_utils/o3d/kpconv.py rename to detectionmetrics/models/lidar_torch_utils/o3d/kpconv.py index 01a0ba29..00a64f28 100644 --- a/detectionmetrics/models/torch_model_utils/o3d/kpconv.py +++ b/detectionmetrics/models/lidar_torch_utils/o3d/kpconv.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Tuple import numpy as np diff --git a/detectionmetrics/models/torch_model_utils/o3d/randlanet.py b/detectionmetrics/models/lidar_torch_utils/o3d/randlanet.py similarity index 100% rename from detectionmetrics/models/torch_model_utils/o3d/randlanet.py rename to detectionmetrics/models/lidar_torch_utils/o3d/randlanet.py diff --git a/detectionmetrics/models/model.py b/detectionmetrics/models/model.py index 45c21a4f..570f0ce0 100644 --- a/detectionmetrics/models/model.py +++ b/detectionmetrics/models/model.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod import os -from typing import Any, List, Optional, Union +from typing import Any, List, Optional, Tuple, Union import numpy as np import pandas as pd @@ -49,6 +49,7 @@ def __init__( self.ontology = uio.read_json(ontology_fname) self.model_cfg = uio.read_json(model_cfg) self.n_classes = len(self.ontology) + self.model_cfg["n_classes"] = self.n_classes @abstractmethod def inference( @@ -89,18 +90,6 @@ def eval( """ raise NotImplementedError - @abstractmethod - def get_computational_cost(self, runs: int = 30, warm_up_runs: int = 5) -> dict: - """Get different metrics related to the computational cost of the model - - :param runs: Number of runs to measure inference time, defaults to 30 - :type runs: int, optional - :param warm_up_runs: Number of warm-up runs, defaults to 5 - :type warm_up_runs: int, optional - :return: Dictionary containing computational cost information - """ - raise NotImplementedError - def get_lut_ontology( self, dataset_ontology: dict, ontology_translation: Optional[str] = None ): @@ -119,6 +108,7 @@ def get_lut_ontology( dataset_ontology, self.ontology, ontology_translation, + classes_to_remove=self.model_cfg.get("classes_to_remove", None), ) return lut_ontology @@ -185,6 +175,24 @@ def eval( """ raise NotImplementedError + @abstractmethod + def get_computational_cost( + self, + image_size: Tuple[int] = None, + runs: int = 30, + warm_up_runs: int = 5, + ) -> dict: + """Get different metrics related to the computational cost of the model + + :param image_size: Image size used for inference + :type image_size: Tuple[int], optional + :param runs: Number of runs to measure inference time, defaults to 30 + :type runs: int, optional + :param warm_up_runs: Number of warm-up runs, defaults to 5 + :type warm_up_runs: int, optional + :return: Dictionary containing computational cost information + """ + raise NotImplementedError class LiDARSegmentationModel(SegmentationModel): """Parent LiDAR segmentation model class @@ -212,11 +220,11 @@ def __init__( super().__init__(model, model_type, model_cfg, ontology_fname, model_fname) @abstractmethod - def inference(self, points: np.ndarray) -> np.ndarray: - """Perform inference for a single image + def inference(self, points_fname: np.ndarray) -> np.ndarray: + """Perform inference for a single point cloud - :param image: Point cloud xyz array - :type image: np.ndarray + :param points_fname: Point cloud in SemanticKITTI .bin format + :type points_fname: str :return: Segmenation result as a point cloud with label indices :rtype: np.ndarray """ @@ -247,3 +255,15 @@ def eval( :rtype: pd.DataFrame """ raise NotImplementedError + + @abstractmethod + def get_computational_cost(self, runs: int = 30, warm_up_runs: int = 5) -> dict: + """Get different metrics related to the computational cost of the model + + :param runs: Number of runs to measure inference time, defaults to 30 + :type runs: int, optional + :param warm_up_runs: Number of warm-up runs, defaults to 5 + :type warm_up_runs: int, optional + :return: Dictionary containing computational cost information + """ + raise NotImplementedError diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index 75c20ddb..80d386b9 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -13,62 +13,24 @@ from detectionmetrics.datasets import dataset as dm_dataset from detectionmetrics.models import model as dm_model -from detectionmetrics.models import torch_model_utils as tmu import detectionmetrics.utils.metrics as um -import detectionmetrics.utils.lidar as ul import detectionmetrics.utils.torch as ut -AVAILABLE_INPUT_FORMATS_LIDAR = ["o3d_randlanet", "o3d_kpconv", "mmdet3d"] +AVAILABLE_MODEL_FORMATS_LIDAR = ["o3d_randlanet", "o3d_kpconv", "mmdet3d"] -def raise_unknown_input_format_lidar(input_format: str) -> None: - """Raise an exception if the LiDAR model input format is unknown +def raise_unknown_model_format_lidar(model_format: str) -> None: + """Raise an exception if the LiDAR model format is unknown - :param input_format: Input format string + :param input_format: Model format string :type input_format: str """ - msg = f"Unknown input format: {input_format}." - msg += f"Available formats: {AVAILABLE_INPUT_FORMATS_LIDAR}" + msg = f"Unknown model format: {model_format}." + msg += f"Available formats: {AVAILABLE_MODEL_FORMATS_LIDAR}" raise Exception(msg) -def get_mmdet3d_sample( - points_fname: str, - label_fname: Optional[str] = None, - name: Optional[str] = None, - idx: Optional[int] = None, - n_feats: int = 4, -) -> dict: - """Get sample data for mmdetection3d models - - :param points_fname: filename of the point cloud - :type points_fname: str - :param label_fname: filename of the semantic label, defaults to None - :type label_fname: Optional[str], optional - :param name: sample name, defaults to None - :type name: Optional[str], optional - :param idx: sample numerical index, defaults to None - :type idx: Optional[int], optional - :param n_feats: number of features, typically [x, y, z, r], defaults to 4 - :type n_feats: int, optional - :return: Sample data dictionary - :rtype: dict - """ - - return { - "lidar_points": { - "lidar_path": points_fname, - "num_pts_feats": n_feats, - }, - "pts_semantic_mask_path": label_fname, - "sample_id": name, - "sample_idx": idx, - "num_pts_feats": n_feats, - "lidar_path": points_fname, - } - - def get_computational_cost( model: Any, dummy_input: torch.Tensor, @@ -256,8 +218,8 @@ class LiDARSegmentationTorchDataset(Dataset): :type dataset: LiDARSegmentationDataset :param model_cfg: Dictionary containing model configuration :type model_cfg: dict - :param preprocess: Function for preprocessing point clouds - :type preprocess: callable + :param get_sample: Function for loading sample data + :type get_sample: callable :param splits: Splits to be used from the dataset, defaults to ["test"] :type splits: str, optional """ @@ -266,101 +228,33 @@ def __init__( self, dataset: dm_dataset.LiDARSegmentationDataset, model_cfg: dict, - preprocess: callable, + get_sample: callable, splits: str = ["test"], ): # Filter split and make filenames global dataset.dataset = dataset.dataset[dataset.dataset["split"].isin(splits)] self.dataset = dataset self.dataset.make_fname_global() - self.model_cfg = model_cfg - self.preprocess = preprocess + self.get_sample = get_sample def __len__(self): return len(self.dataset.dataset) - def __getitem__( - self, idx: int - ) -> Tuple[Union[np.ndarray, torch.Tensor], Union[np.ndarray, torch.Tensor]]: - """Prepare sample data: point cloud and label + def __getitem__(self, idx: int): + """Prepare sample data :param idx: Sample index :type idx: int - :return: Sample index, point cloud, projected indices, semantic label, and sampler - :rtype: Tuple[str, np.ndarray, np.ndarray, np.ndarray, ul.Sampler] + :return: Sample data required by the model """ - # Read the point cloud and its labels - points = self.dataset.read_points(self.dataset.dataset.iloc[idx]["points"]) - semantic_label = self.dataset.read_label( - self.dataset.dataset.iloc[idx]["label"] - ) - - # Preprocess point cloud - preprocessed_points, projected_indices, sampler = self.preprocess( - points, self.model_cfg - ) - - return ( - self.dataset.dataset.index[idx], - preprocessed_points, - projected_indices, - semantic_label, - sampler, - ) - - -class LiDARSegmentationMMDet3DDataset(Dataset): - """Dataset for LiDAR segmentation PyTorch - mmdetection3d models - - :param dataset: LiDAR segmentation dataset - :type dataset: LiDARSegmentationDataset - :param model_cfg: Dictionary containing model configuration - :type model_cfg: dict - :param preprocess: Function for preprocessing point clouds - :type preprocess: callable - :param n_classes: Number of classes estimated by the model - :type n_classes: int - :param splits: Splits to be used from the dataset, defaults to ["test"] - :type splits: str, optional - """ - - def __init__( - self, - dataset: dm_dataset.LiDARSegmentationDataset, - model_cfg: dict, - preprocess: callable, - splits: str = ["test"], - ): - # Filter split and make filenames global - dataset.dataset = dataset.dataset[dataset.dataset["split"].isin(splits)] - self.dataset = dataset - self.dataset.make_fname_global() - - self.model_cfg = model_cfg - self.preprocess = preprocess - - def __len__(self): - return len(self.dataset.dataset) - - def __getitem__( - self, idx: int - ) -> Tuple[Union[np.ndarray, torch.Tensor], Union[np.ndarray, torch.Tensor]]: - """Prepare sample data: point cloud and label - - :param idx: Sample index - :type idx: int - :return: Point cloud and corresponding label tensor or numpy arrays - :rtype: Tuple[np.ndarray, np.ndarray,] - """ - sample = get_mmdet3d_sample( + return self.get_sample( points_fname=self.dataset.dataset.iloc[idx]["points"], + model_cfg=self.model_cfg, label_fname=self.dataset.dataset.iloc[idx]["label"], name=self.dataset.dataset.index[idx], idx=idx, - n_feats=self.model_cfg.get("n_feats", 4), ) - return self.preprocess(sample) class TorchImageSegmentationModel(dm_model.ImageSegmentationModel): @@ -701,35 +595,21 @@ def __init__( self.model = self.model.to(self.device).eval() # Init specific attributes and update model configuration - self.end_th = self.model_cfg.get("end_th", 0.5) - self.input_format = self.model_cfg["input_format"] - self.model_cfg["n_classes"] = self.n_classes + self.model_format = self.model_cfg["model_format"] # Init model specific functions - if "o3d" in self.input_format: # Open3D-ML - self.preprocess = tmu.o3d.preprocess - self.transform_output = ( - lambda x: torch.argmax(x.squeeze(), axis=-1).squeeze().cpu().numpy() - ) - self._inference = tmu.o3d.inference - if self.input_format == "o3d_randlanet": # Open3D RandLaNet - self.transform_input = tmu.o3d.randlanet.transform_input - self.update_probs = tmu.o3d.randlanet.update_probs - decoder_layers = self.model.decoder.children() - self.model_cfg["num_layers"] = sum(1 for _ in decoder_layers) - elif self.input_format == "o3d_kpconv": # Open3D KPConv - self.transform_input = tmu.o3d.kpconv.transform_input - self.update_probs = tmu.o3d.kpconv.update_probs - else: - raise raise_unknown_input_format_lidar(self.input_format) - elif self.input_format == "mmdet3d": - self.preprocess = tmu.mmdet3d.preprocess - self._inference = tmu.mmdet3d.inference - self.transform_input = None - self.update_probs = None - self.transform_output = lambda x: x.cpu().numpy() + if self.model_format == "mmdet3d": + from detectionmetrics.models.lidar_torch_utils import mmdet3d + + self._get_sample = mmdet3d.get_sample + self._inference = mmdet3d.inference + elif "o3d" in self.model_format: + from detectionmetrics.models.lidar_torch_utils import o3d + + self._get_sample = o3d.get_sample + self._inference = o3d.inference else: - raise raise_unknown_input_format_lidar(self.input_format) + raise raise_unknown_model_format_lidar(self.model_format) def inference(self, points_fname: str) -> np.ndarray: """Perform inference for a single point cloud @@ -740,23 +620,10 @@ def inference(self, points_fname: str) -> np.ndarray: :rtype: np.ndarray """ # Preprocess point cloud + sample = self._get_sample(points_fname, self.model_cfg) + pred, _, _ = self._inference(sample, self.model, self.model_cfg) - if "o3d" in self.input_format: - points = ul.read_semantickitti_points(points_fname) - sample = self.preprocess(points, self.model_cfg) - points_fname, projected_indices, sampler = sample - pred = self._inference(points_fname, projected_indices, sampler, self) - elif self.input_format == "mmdet3d": - sample = get_mmdet3d_sample( - points_fname=points_fname, n_feats=self.model_cfg.get("n_feats", 4) - ) - sample = self.preprocess(sample) - pred = self._inference(sample, self.model) - pred = pred.pred_pts_seg.pts_semantic_mask - else: - raise_unknown_input_format_lidar(self.input_format) - - return self.transform_output(pred) + return pred.squeeze().cpu().numpy() def eval( self, @@ -800,16 +667,11 @@ def eval( for ignored_class in self.model_cfg.get("ignored_classes", []): ignored_label_indices.append(dataset.ontology[ignored_class]["idx"]) - # Get PyTorch dataset (no dataloader to avoid complexity with batching samplers) - dataset_type = ( - LiDARSegmentationMMDet3DDataset - if self.input_format == "mmdet3d" - else LiDARSegmentationTorchDataset - ) - dataset = dataset_type( + # Get PyTorch dataloader + dataset = LiDARSegmentationTorchDataset( dataset, - model_cfg=self.model_cfg, - preprocess=self.preprocess, + self.model_cfg, + self._get_sample, splits=[split] if isinstance(split, str) else split, ) @@ -821,23 +683,7 @@ def eval( pbar = tqdm(dataset, total=len(dataset), leave=True) for sample in pbar: # Perform inference - if "o3d" in self.input_format: - name, points, projected_indices, label, sampler = sample - label = torch.tensor(label, device=self.device) - pred = self._inference(points, projected_indices, sampler, self) - elif self.input_format == "mmdet3d": - pred_samples = self._inference(sample, self.model) - if not isinstance(pred_samples, list): - pred_samples = [pred_samples] - pred, label = [], [] - for pred_sample in pred_samples: - name = pred_sample.metainfo["sample_id"] - pred.append(pred_sample.pred_pts_seg.pts_semantic_mask) - label.append(pred_sample.gt_pts_seg.pts_semantic_mask) - pred = torch.stack(pred, dim=0) - label = torch.stack(label, dim=0) - else: - raise_unknown_input_format_lidar(self.input_format) + pred, label, name = self._inference(sample, self.model, self.model_cfg) # Get valid points masks depending on ignored label indices if ignored_label_indices: @@ -852,10 +698,10 @@ def eval( label = lut_ontology[label] # Prepare data and update metrics factory - label = label.cpu().unsqueeze(0).numpy() - pred = pred.cpu().unsqueeze(0).to(torch.int64).numpy() + label = label.cpu().numpy() + pred = pred.cpu().numpy() if valid_mask is not None: - valid_mask = valid_mask.cpu().unsqueeze(0).numpy() + valid_mask = valid_mask.cpu().numpy() metrics_factory.update(pred, label, valid_mask) @@ -899,7 +745,7 @@ def get_computational_cost(self, runs: int = 30, warm_up_runs: int = 5) -> dict: dummy_input, _ = self.transform_input(dummy_points, self.model_cfg, sampler) dummy_input = ut.data_to_device(dummy_input, self.device) - if self.input_format != "o3d_kpconv": + if self.model_format != "o3d_kpconv": dummy_input = ut.unsqueeze_data(dummy_input) # Get computational cost diff --git a/detectionmetrics/models/torch_model_utils/__init__.py b/detectionmetrics/models/torch_model_utils/__init__.py deleted file mode 100644 index 33210463..00000000 --- a/detectionmetrics/models/torch_model_utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from detectionmetrics.models.torch_model_utils import mmdet3d, o3d diff --git a/detectionmetrics/models/torch_model_utils/o3d/__init__.py b/detectionmetrics/models/torch_model_utils/o3d/__init__.py deleted file mode 100644 index c7a1aa5d..00000000 --- a/detectionmetrics/models/torch_model_utils/o3d/__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations # used to avoid circular import in type annotation -from typing import Tuple - - -import numpy as np -import torch - -try: - from open3d._ml3d.datasets.utils import DataProcessing -except Exception: - print("Open3D-ML3D not available") -from sklearn.neighbors import KDTree - -from detectionmetrics.models.torch_model_utils.o3d import kpconv, randlanet -from detectionmetrics.utils import lidar as ul -import detectionmetrics.utils.torch as ut - - -def preprocess( - points: np.ndarray, cfg: dict -) -> Tuple[np.ndarray, np.ndarray, ul.Sampler]: - """Preprocess point cloud data - - :param points: Point cloud data - :type points: np.ndarray - :param cfg: Dictionary containing model configuration, defaults to {} - :type cfg: dict - :return: Subsampled points, projected indices and sampler - :rtype: Tuple[np.ndarray, np.ndarray, ul.Sampler] - """ - # Keep only XYZ coordinates - points = np.array(points[:, 0:3], dtype=np.float32) - - # Subsample points using a grid of given size - grid_size = cfg.get("grid_size", 0.06) - sub_points = DataProcessing.grid_subsampling(points, grid_size=grid_size) - - # Create search tree so that we can project points back to the original point cloud - search_tree = KDTree(sub_points) - projected_indices = np.squeeze(search_tree.query(points, return_distance=False)) - projected_indices = projected_indices.astype(np.int32) - - # Init sampler - sampler = None - if "sampler" in cfg: - sampler = ul.Sampler( - sub_points.shape[0], search_tree, cfg["sampler"], cfg["n_classes"] - ) - - return sub_points, projected_indices, sampler - - -def inference( - points: np.ndarray, - projected_indices: np.ndarray, - sampler: ul.Sampler, - model: TorchLiDARSegmentationModel, # type: ignore (future annotation) -) -> torch.Tensor: - """Perform inference on the point cloud data - :param points: Point cloud data - :type points: np.ndarray - :param projected_indices: Indices of the projected points - :type projected_indices: np.ndarray - :param sampler: Sampler object for sampling point cloud - :type sampler: ul.Sampler - :param model: Model object for inference - :type model: TorchLiDARSegmentationModel - :return: Inference result - :rtype: torch.Tensor - """ - infer_complete = False - while not infer_complete: - # Get model input data - input_data, selected_indices = model.transform_input( - points, model.model_cfg, sampler - ) - input_data = ut.data_to_device(input_data, model.device) - if model.input_format != "o3d_kpconv": - input_data = ut.unsqueeze_data(input_data) - - # Perform inference - with torch.no_grad(): - pred = model.model(*input_data) - - # TODO: check if this is consistent across different models - if isinstance(pred, dict): - pred = pred["out"] - - # Update probabilities if sampler is used - if sampler is not None: - if model.input_format == "o3d_kpconv": - sampler.test_probs = model.update_probs( - pred, - selected_indices, - sampler.test_probs, - lengths=input_data[-1], - ) - else: - sampler.test_probs = model.update_probs( - pred, - selected_indices, - sampler.test_probs, - model.n_classes, - ) - if sampler.p[sampler.p > model.end_th].shape[0] == sampler.p.shape[0]: - pred = sampler.test_probs[projected_indices] - infer_complete = True - else: - pred = pred.squeeze().cpu()[projected_indices].cuda() - infer_complete = True - return pred From 1a5914fc5cc56ea09b4c41e0cd2ad57d905d6c3d Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 19 May 2025 11:16:36 +0200 Subject: [PATCH 11/39] Improve compatibility with previous Python versions --- detectionmetrics/models/model.py | 12 +++---- detectionmetrics/models/tensorflow.py | 4 +-- detectionmetrics/models/torch.py | 17 ++++++---- detectionmetrics/utils/metrics.py | 46 +++++++++++++-------------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/detectionmetrics/models/model.py b/detectionmetrics/models/model.py index 570f0ce0..07d7bf4d 100644 --- a/detectionmetrics/models/model.py +++ b/detectionmetrics/models/model.py @@ -68,7 +68,7 @@ def inference( def eval( self, dataset: dm_dataset.SegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -78,7 +78,7 @@ def eval( :param dataset: Segmentation dataset for which the evaluation will be performed :type dataset: ImageSegmentationDataset :param split: Split or splits to be used from the dataset, defaults to "test" - :type split: str | List[str], optional + :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. @@ -153,7 +153,7 @@ def inference(self, image: Image.Image) -> Image.Image: def eval( self, dataset: dm_dataset.ImageSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -163,7 +163,7 @@ def eval( :param dataset: Image segmentation dataset for which the evaluation will be performed :type dataset: ImageSegmentationDataset :param split: Split or splits to be used from the dataset, defaults to "test" - :type split: str | List[str], optional + :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. @@ -234,7 +234,7 @@ def inference(self, points_fname: np.ndarray) -> np.ndarray: def eval( self, dataset: dm_dataset.LiDARSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -244,7 +244,7 @@ def eval( :param dataset: LiDAR segmentation dataset for which the evaluation will be performed :type dataset: LiDARSegmentationDataset :param split: Split or splits to be used from the dataset, defaults to "test" - :type split: str | List[str], optional + :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. diff --git a/detectionmetrics/models/tensorflow.py b/detectionmetrics/models/tensorflow.py index e595f97a..5679ccee 100644 --- a/detectionmetrics/models/tensorflow.py +++ b/detectionmetrics/models/tensorflow.py @@ -386,7 +386,7 @@ def inference(self, image: Image.Image) -> Image.Image: def eval( self, dataset: ImageSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -396,7 +396,7 @@ def eval( :param dataset: Image segmentation dataset for which the evaluation will be performed :type dataset: ImageSegmentationDataset :param split: Split to be used from the dataset, defaults to "test" - :type split: str | List[str], optional + :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index 80d386b9..a693d150 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -7,8 +7,13 @@ from PIL import Image import torch from torch.utils.data import DataLoader, Dataset -from torchvision.transforms import v2 as transforms -from torchvision.transforms.v2 import functional as F + +try: + from torchvision.transforms import v2 as transforms + from torchvision.transforms.v2 import functional as F +except ImportError: + from torchvision.transforms import transforms + from torchvision.transforms import functional as F from tqdm import tqdm from detectionmetrics.datasets import dataset as dm_dataset @@ -403,7 +408,7 @@ def inference(self, image: Image.Image) -> Image.Image: def eval( self, dataset: dm_dataset.ImageSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -413,7 +418,7 @@ def eval( :param dataset: Image segmentation dataset for which the evaluation will be performed :type dataset: ImageSegmentationDataset :param split: Split or splits to be used from the dataset, defaults to "test" - :type split: str | List[str], optional + :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. @@ -628,7 +633,7 @@ def inference(self, points_fname: str) -> np.ndarray: def eval( self, dataset: dm_dataset.LiDARSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -638,7 +643,7 @@ def eval( :param dataset: LiDAR segmentation dataset for which the evaluation will be performed :type dataset: LiDARSegmentationDataset :param split: Split or splits to be used from the dataset, defaults to "test" - :type split: str | List[str], optional + :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. diff --git a/detectionmetrics/utils/metrics.py b/detectionmetrics/utils/metrics.py index 1b31e588..1d563b34 100644 --- a/detectionmetrics/utils/metrics.py +++ b/detectionmetrics/utils/metrics.py @@ -1,6 +1,6 @@ from collections import defaultdict import math -from typing import Optional +from typing import List, Optional, Union import numpy as np import pandas as pd @@ -61,11 +61,11 @@ def update( ) self.confusion_matrix += new_entry.reshape(self.n_classes, self.n_classes) - def get_metric_names(self) -> list[str]: + def get_metric_names(self) -> List[str]: """Get available metric names :return: List of available metric names - :rtype: list[str] + :rtype: List[str] """ return self.METRIC_NAMES @@ -77,58 +77,58 @@ def get_confusion_matrix(self) -> np.ndarray: """ return self.confusion_matrix - def get_tp(self, per_class: bool = True) -> np.ndarray | int: + def get_tp(self, per_class: bool = True) -> Union[np.ndarray, int]: """True Positives :param per_class: Return per class TP, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, int] """ tp = np.diag(self.confusion_matrix) return tp if per_class else int(np.nansum(tp)) - def get_fp(self, per_class: bool = True) -> np.ndarray | int: + def get_fp(self, per_class: bool = True) -> Union[np.ndarray, int]: """False Positives :param per_class: Return per class FP, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, int] """ fp = self.confusion_matrix.sum(axis=0) - np.diag(self.confusion_matrix) return fp if per_class else int(np.nansum(fp)) - def get_fn(self, per_class: bool = True) -> np.ndarray | int: + def get_fn(self, per_class: bool = True) -> Union[np.ndarray, int]: """False negatives :param per_class: Return per class FN, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, int] """ fn = self.confusion_matrix.sum(axis=1) - np.diag(self.confusion_matrix) return fn if per_class else int(np.nansum(fn)) - def get_tn(self, per_class: bool = True) -> np.ndarray | int: + def get_tn(self, per_class: bool = True) -> Union[np.ndarray, int]: """True negatives :param per_class: Return per class TN, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, int] """ total = self.confusion_matrix.sum() tn = total - (self.get_tp() + self.get_fp() + self.get_fn()) return tn if per_class else int(np.nansum(tn)) - def get_precision(self, per_class: bool = True) -> np.ndarray | float: + def get_precision(self, per_class: bool = True) -> Union[np.ndarray, float]: """Precision = TP / (TP + FP) :param per_class: Return per class precision, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, float] """ tp = self.get_tp(per_class) fp = self.get_fp(per_class) @@ -139,13 +139,13 @@ def get_precision(self, per_class: bool = True) -> np.ndarray | float: else: return np.where(denominator > 0, tp / denominator, np.nan) - def get_recall(self, per_class: bool = True) -> np.ndarray | float: + def get_recall(self, per_class: bool = True) -> Union[np.ndarray, float]: """Recall = TP / (TP + FN) :param per_class: Return per class recall, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, float] """ tp = self.get_tp(per_class) fn = self.get_fn(per_class) @@ -156,13 +156,13 @@ def get_recall(self, per_class: bool = True) -> np.ndarray | float: else: return np.where(denominator > 0, tp / denominator, np.nan) - def get_accuracy(self, per_class: bool = True) -> np.ndarray | float: + def get_accuracy(self, per_class: bool = True) -> Union[np.ndarray, float]: """Accuracy = (TP + TN) / (TP + FP + FN + TN) :param per_class: Return per class accuracy, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, float] """ tp = self.get_tp(per_class) fp = self.get_fp(per_class) @@ -175,13 +175,13 @@ def get_accuracy(self, per_class: bool = True) -> np.ndarray | float: else: return np.where(total > 0, (tp + tn) / total, np.nan) - def get_f1_score(self, per_class: bool = True) -> np.ndarray | float: + def get_f1_score(self, per_class: bool = True) -> Union[np.ndarray, float]: """F1-score = 2 * (Precision * Recall) / (Precision + Recall) :param per_class: Return per class F1 score, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, float] """ precision = self.get_precision(per_class) recall = self.get_recall(per_class) @@ -196,13 +196,13 @@ def get_f1_score(self, per_class: bool = True) -> np.ndarray | float: denominator > 0, 2 * (precision * recall) / denominator, np.nan ) - def get_iou(self, per_class: bool = True) -> np.ndarray | float: + def get_iou(self, per_class: bool = True) -> Union[np.ndarray, float]: """IoU = TP / (TP + FP + FN) :param per_class: Return per class IoU, defaults to True :type per_class: bool, optional :return: True Positives - :rtype: np.ndarray | int + :rtype: Union[np.ndarray, float] """ tp = self.get_tp(per_class) fp = self.get_fp(per_class) @@ -242,7 +242,7 @@ def get_averaged_metric( def get_metric_per_name( self, metric_name: str, per_class: bool = True - ) -> np.ndarray | float | int: + ) -> Union[np.ndarray, float, int]: """Get metric value by name :param metric_name: Name of the metric to compute @@ -250,7 +250,7 @@ def get_metric_per_name( :param per_class: Return per class metric, defaults to True :type per_class: bool, optional :return: Metric value - :rtype: np.ndarray | float | int + :rtype: Union[np.ndarray, float, int] """ return getattr(self, f"get_{metric_name}")(per_class=per_class) From 9454df7fa1039504da4e377a4ceb3c44af22f805 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 19 May 2025 11:17:03 +0200 Subject: [PATCH 12/39] Add support for LSK3DNet and SphereFormer --- detectionmetrics/datasets/gaia.py | 11 +- .../models/lidar_torch_utils/__init__.py | 20 +- .../models/lidar_torch_utils/lsk3dnet.py | 298 +++++++++++++++--- .../models/lidar_torch_utils/sphereformer.py | 161 ++++++++++ detectionmetrics/models/torch.py | 23 +- 5 files changed, 455 insertions(+), 58 deletions(-) create mode 100644 detectionmetrics/models/lidar_torch_utils/sphereformer.py diff --git a/detectionmetrics/datasets/gaia.py b/detectionmetrics/datasets/gaia.py index 3c095e09..d80ae385 100644 --- a/detectionmetrics/datasets/gaia.py +++ b/detectionmetrics/datasets/gaia.py @@ -23,8 +23,15 @@ def build_dataset(dataset_fname: str) -> Tuple[pd.DataFrame, str, dict]: dataset_dir = os.path.dirname(dataset_fname) # Read ontology file - ontology_fname = dataset.attrs["ontology_fname"] - ontology = uio.read_json(os.path.join(dataset_dir, ontology_fname)) + try: + ontology_fname = dataset.attrs["ontology_fname"] + except KeyError: + ontology_fname = "ontology.json" + + ontology_fname = os.path.join(dataset_dir, ontology_fname) + assert os.path.isfile(ontology_fname), "Ontology file not found" + + ontology = uio.read_json(ontology_fname) for name, data in ontology.items(): ontology[name]["rgb"] = tuple(data["rgb"]) diff --git a/detectionmetrics/models/lidar_torch_utils/__init__.py b/detectionmetrics/models/lidar_torch_utils/__init__.py index 5bb904a0..c2b488a4 100644 --- a/detectionmetrics/models/lidar_torch_utils/__init__.py +++ b/detectionmetrics/models/lidar_torch_utils/__init__.py @@ -1 +1,19 @@ -from detectionmetrics.models.lidar_torch_utils import o3d, mmdet3d \ No newline at end of file +try: + from detectionmetrics.models.lidar_torch_utils import o3d +except ImportError: + pass + +try: + from detectionmetrics.models.lidar_torch_utils import mmdet3d +except ImportError: + pass + +try: + from detectionmetrics.models.lidar_torch_utils import lsk3dnet +except ImportError: + pass + +try: + from detectionmetrics.models.lidar_torch_utils import sphereformer +except ImportError: + pass diff --git a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py index b769cbb3..5e49a67c 100644 --- a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py +++ b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py @@ -1,50 +1,262 @@ -from mmdet3d.datasets.transforms import ( - LoadPointsFromFile, - LoadAnnotations3D, - Pack3DDetInputs, -) -from mmengine.registry import FUNCTIONS -from torchvision.transforms import Compose - -COLLATE_FN = FUNCTIONS.get("pseudo_collate") - - -def preprocess(sample): - n_feats = sample["num_pts_feats"] - transforms = [ - LoadPointsFromFile(coord_type="LIDAR", load_dim=4, use_dim=n_feats) - ] - if sample["pts_semantic_mask_path"] is not None: - transforms.append( - LoadAnnotations3D( - with_bbox_3d=False, - with_label_3d=False, - with_seg_3d=True, - seg_3d_dtype="np.uint32", - seg_offset=65536, - dataset_type="semantickitti", - ) - ) - transforms.append( - Pack3DDetInputs( - keys=["points", "pts_semantic_mask"], - meta_keys=["sample_idx", "lidar_path", "num_pts_feats", "sample_id"], - ) +from typing import List, Optional, Tuple + +from c_gen_normal_map import gen_normal_map +import numpy as np +import torch +import utils.depth_map_utils as depth_map_utils + +import detectionmetrics.utils.torch as ut + + +def range_projection(current_vertex, fov_up=3.0, fov_down=-25.0, proj_H=64, proj_W=900): + """Project a pointcloud into a spherical projection (range image).""" + # laser parameters + fov_up = fov_up / 180.0 * np.pi # field of view up in radians + fov_down = fov_down / 180.0 * np.pi # field of view down in radians + fov = abs(fov_down) + abs(fov_up) # get field of view total in radians + + # get depth of all points + depth = np.linalg.norm(current_vertex[:, :3], 2, axis=1) + + # get scan components + scan_x = current_vertex[:, 0] + scan_y = current_vertex[:, 1] + scan_z = current_vertex[:, 2] + + # get angles of all points + yaw = -np.arctan2(scan_y, scan_x) + pitch = np.arcsin(scan_z / depth) + + # get projections in image coords + proj_x = 0.5 * (yaw / np.pi + 1.0) # in [0.0, 1.0] + proj_y = 1.0 - (pitch + abs(fov_down)) / fov # in [0.0, 1.0] + + # scale to image size using angular resolution + proj_x *= proj_W # in [0.0, W] + proj_y *= proj_H # in [0.0, H] + + # round and clamp for use as index + proj_x = np.floor(proj_x) + proj_x = np.minimum(proj_W - 1, proj_x) + proj_x = np.maximum(0, proj_x).astype(np.int32) # in [0,W-1] + from_proj_x = np.copy(proj_x) # store a copy in orig order + + proj_y = np.floor(proj_y) + proj_y = np.minimum(proj_H - 1, proj_y) + proj_y = np.maximum(0, proj_y).astype(np.int32) # in [0,H-1] + from_proj_y = np.copy(proj_y) # stope a copy in original order + + # order in decreasing depth + order = np.argsort(depth)[::-1] + depth = depth[order] + + proj_y = proj_y[order] + proj_x = proj_x[order] + + scan_x = scan_x[order] + scan_y = scan_y[order] + scan_z = scan_z[order] + + indices = np.arange(depth.shape[0]) + indices = indices[order] + + proj_range = np.full((proj_H, proj_W), -1, dtype=np.float32) + proj_vertex = np.full((proj_H, proj_W, 4), -1, dtype=np.float32) + proj_idx = np.full((proj_H, proj_W), -1, dtype=np.int32) + + proj_range[proj_y, proj_x] = depth + proj_vertex[proj_y, proj_x] = np.array( + [scan_x, scan_y, scan_z, np.ones(len(scan_x))] + ).T + proj_idx[proj_y, proj_x] = indices + + return proj_range, proj_vertex, from_proj_x, from_proj_y + + +def compute_normals_range( + current_vertex, proj_H=64, proj_W=900, extrapolate=True, blur_type="gaussian" +): + """Compute normals for each point using range image-based method.""" + proj_range, proj_vertex, from_proj_x, from_proj_y = range_projection(current_vertex) + proj_range = depth_map_utils.fill_in_fast( + proj_range, extrapolate=extrapolate, blur_type=blur_type ) - transforms = Compose(transforms) - return transforms(sample) + # generate normal image + normal_data = gen_normal_map(proj_range, proj_vertex, proj_H, proj_W) + unproj_normal_data = normal_data[from_proj_y, from_proj_x] -def inference(sample, model): - single_sample = not isinstance(sample["data_samples"], list) - if single_sample: - sample = COLLATE_FN([sample]) + return unproj_normal_data + + +def collate_fn(samples: List[dict]) -> dict: + """Collate function for batching samples + + :param samples: list of sample dictionaries + :type samples: List[dict] + :return: collated batch dictionary + :rtype: dict + """ + point_num = [d["point_num"] for d in samples] + batch_size = len(point_num) + ref_labels = samples[0]["ref_label"] + origin_len = samples[0]["origin_len"] + ref_indices = [torch.from_numpy(d["ref_index"]) for d in samples] + path = samples[0]["root"] # [d['root'] for d in data] + root = [d["root"] for d in samples] + sample_id = [d["sample_id"] for d in samples] + + b_idx = [] + for i in range(batch_size): + b_idx.append(torch.ones(point_num[i]) * i) + points = [torch.from_numpy(d["point_feat"]) for d in samples] + ref_xyz = [torch.from_numpy(d["ref_xyz"]) for d in samples] - sample = model.data_preprocessor(sample, training=False) - inputs, data_samples = sample["inputs"], sample["data_samples"] - pred = model(inputs, data_samples, mode="predict") + has_labels = samples[0]["point_label"] is not None + if has_labels: + labels = [torch.from_numpy(d["point_label"]) for d in samples] + else: + labels = [d["point_label"] for d in samples] + normal = [torch.from_numpy(d["normal"]) for d in samples] + return { + "points": torch.cat(points).float(), + "normal": torch.cat(normal).float(), + "ref_xyz": torch.cat(ref_xyz).float(), + "batch_idx": torch.cat(b_idx).long(), + "batch_size": batch_size, + "labels": torch.cat(labels).long().squeeze(1) if has_labels else labels, + "raw_labels": torch.from_numpy(ref_labels).long() if has_labels else ref_labels, + "origin_len": origin_len, + "indices": torch.cat(ref_indices).long(), + "path": path, + "point_num": point_num, + "root": root, + "sample_id": sample_id, + } + + +def get_sample( + points_fname: str, + model_cfg: dict, + label_fname: Optional[str] = None, + name: Optional[str] = None, + idx: Optional[int] = None, +) -> dict: + """Get sample data for mmdetection3d models + + :param points_fname: filename of the point cloud + :type points_fname: str + :param model_cfg: model configuration + :type model_cfg: dict + :param label_fname: filename of the semantic label, defaults to None + :type label_fname: Optional[str], optional + :param name: sample name, defaults to None + :type name: Optional[str], optional + :param idx: sample numerical index, defaults to None + :type idx: Optional[int], optional + :return: Sample data dictionary + :rtype: dict + """ + raw_data = np.fromfile(points_fname, dtype=np.float32) + raw_data = raw_data.reshape((-1, 4)) + + labels, ref_labels = None, None + if label_fname is not None: + annotated_data = np.fromfile(label_fname, dtype=np.uint32) + annotated_data = annotated_data.reshape((-1, 1)) + labels = annotated_data & 0xFFFF # delete high 16 digits binary + labels = labels.astype(np.uint8) + ref_labels = labels.copy() + + xyz = raw_data[:, :3] + feat = raw_data[:, 3:4] if model_cfg["n_feats"] > 3 else None + origin_len = len(xyz) + + ref_pc = xyz.copy() + ref_index = np.arange(len(ref_pc)) + + mask_x = np.logical_and( + xyz[:, 0] > model_cfg["min_volume_space"][0], + xyz[:, 0] < model_cfg["max_volume_space"][0], + ) + mask_y = np.logical_and( + xyz[:, 1] > model_cfg["min_volume_space"][1], + xyz[:, 1] < model_cfg["max_volume_space"][1], + ) + mask_z = np.logical_and( + xyz[:, 2] > model_cfg["min_volume_space"][2], + xyz[:, 2] < model_cfg["max_volume_space"][2], + ) + mask = np.logical_and(mask_x, np.logical_and(mask_y, mask_z)) + + not_zero = np.logical_not(np.all(xyz[:, :3] == 0, axis=1)) + mask = np.logical_and(mask, not_zero) + + xyz = xyz[mask] + if labels is not None: + labels = labels[mask] + ref_index = ref_index[mask] + if feat is not None: + feat = feat[mask] + point_num = len(xyz) + + feat = np.concatenate((xyz, feat), axis=1) if feat is not None else xyz + + unproj_normal_data = compute_normals_range(feat) + + sample = {} + sample["point_feat"] = feat + sample["point_label"] = labels + sample["ref_xyz"] = ref_pc + sample["ref_label"] = ref_labels + sample["ref_index"] = ref_index + sample["point_num"] = point_num + sample["origin_len"] = origin_len + sample["normal"] = unproj_normal_data + sample["root"] = points_fname + sample["sample_id"] = name + sample["idx"] = idx + + return sample + + +def inference( + sample: dict, model: torch.nn.Module, model_cfg: dict +) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: + """Perform inference on a sample using an mmdetection3D model + + :param sample: sample data dictionary + :type sample: dict + :param model: mmdetection3D model + :type model: torch.nn.Module + :param model_cfg: model configuration + :type model_cfg: dict + :return: predictions, labels, and sample names + :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + """ + single_sample = not isinstance(sample["sample_id"], list) if single_sample: - pred = pred[0] + sample = collate_fn([sample]) + + device = next(model.parameters()).device + for k, v in sample.items(): + sample[k] = ut.data_to_device(v, device) + + pred = model(sample) + pred["logits"] = torch.argmax(pred["logits"], dim=1) + + has_labels = pred["labels"][0] is not None + preds, labels, names = ([], [], []) if has_labels else ([], None, None) + + for batch_idx in range(pred["batch_size"]): + preds.append(pred["logits"][pred["batch_idx"] == batch_idx]) + if has_labels: + labels.append(pred["labels"][pred["batch_idx"] == batch_idx]) + names.append(pred["sample_id"][batch_idx]) + + preds = torch.stack(preds, dim=0).squeeze() + if has_labels: + labels = torch.stack(labels, dim=0).squeeze() - return pred + return preds, labels, names diff --git a/detectionmetrics/models/lidar_torch_utils/sphereformer.py b/detectionmetrics/models/lidar_torch_utils/sphereformer.py new file mode 100644 index 00000000..a92adcd9 --- /dev/null +++ b/detectionmetrics/models/lidar_torch_utils/sphereformer.py @@ -0,0 +1,161 @@ +from typing import List, Optional, Tuple + +import numpy as np +import spconv.pytorch as spconv +import torch +from util.data_util import data_prepare + +import detectionmetrics.utils.torch as ut + + +def collate_fn(samples: List[dict]) -> dict: + """Collate function for batching samples + + :param samples: list of sample dictionaries + :type samples: List[dict] + :return: collated batch dictionary + :rtype: dict + """ + coords, xyz, feats, labels, inds_recons, fnames, sample_ids = list(zip(*samples)) + inds_recons = list(inds_recons) + + accmulate_points_num = 0 + offset = [] + for i in range(len(coords)): + inds_recons[i] = accmulate_points_num + inds_recons[i] + accmulate_points_num += coords[i].shape[0] + offset.append(accmulate_points_num) + + coords = torch.cat(coords) + xyz = torch.cat(xyz) + feats = torch.cat(feats) + labels = torch.cat(labels) + offset = torch.IntTensor(offset) + inds_recons = torch.cat(inds_recons) + + return ( + coords, + xyz, + feats, + labels, + offset, + inds_recons, + list(fnames), + list(sample_ids), + ) + + +def get_sample( + points_fname: str, + model_cfg: dict, + label_fname: Optional[str] = None, + name: Optional[str] = None, + idx: Optional[int] = None, +) -> dict: + """Get sample data for mmdetection3d models + + :param points_fname: filename of the point cloud + :type points_fname: str + :param model_cfg: model configuration + :type model_cfg: dict + :param label_fname: filename of the semantic label, defaults to None + :type label_fname: Optional[str], optional + :param name: sample name, defaults to None + :type name: Optional[str], optional + :param idx: sample numerical index, defaults to None + :type idx: Optional[int], optional + :return: Sample data dictionary + :rtype: dict + """ + feats = np.fromfile(points_fname, dtype=np.float32) + feats = feats.reshape((-1, 4))[:, : model_cfg["n_feats"]] + + annotated_data = np.fromfile(label_fname, dtype=np.uint32) + annotated_data = annotated_data.reshape((-1, 1)) + labels = annotated_data & 0xFFFF # delete high 16 digits binary + labels = labels.astype(np.uint8) + instance_label = annotated_data >> 16 + instance_label = instance_label.reshape(-1) + + xyz = feats[:, :3] + + xyz = np.clip(xyz, model_cfg["pc_range"][0], model_cfg["pc_range"][1]) + + labels_in = annotated_data.astype(np.uint8).reshape(-1) + + coords, xyz, feats, labels, inds_reconstruct = data_prepare( + xyz, + feats, + labels_in, + "test", + np.array(model_cfg["voxel_size"]), + model_cfg["voxel_max"], + None, + model_cfg["xyz_norm"], + ) + + sample = ( + coords, + xyz, + feats, + labels, + inds_reconstruct, + points_fname, + name, + ) + + return sample + + +def inference( + sample: dict, model: torch.nn.Module, model_cfg: dict +) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: + """Perform inference on a sample using an mmdetection3D model + + :param sample: sample data dictionary + :type sample: dict + :param model: mmdetection3D model + :type model: torch.nn.Module + :param model_cfg: model configuration + :type model_cfg: dict + :return: predictions, labels, and sample names + :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + """ + single_sample = not isinstance(sample[-1], list) + if single_sample: + sample = collate_fn([sample]) + + device = next(model.parameters()).device + sample = ut.data_to_device(sample, device) + + ( + coord, + xyz, + feat, + labels, + offset, + inds_reconstruct, + fnames, + names, + ) = sample + + offset_ = offset.clone() + offset_[1:] = offset_[1:] - offset_[:-1] + + batch = ( + torch.cat([torch.tensor([ii] * o) for ii, o in enumerate(offset_)], 0) + .long() + .to(device) + ) + + coord = torch.cat([batch.unsqueeze(-1), coord], -1) + spatial_shape = np.clip((coord.max(0)[0][1:] + 1).cpu().numpy(), 128, None) + batch_size = len(fnames) + + sinput = spconv.SparseConvTensor(feat, coord.int(), spatial_shape, batch_size) + + preds = model(sinput, xyz, batch) + preds = preds[inds_reconstruct, :] + preds = torch.argmax(preds, dim=1) + + return preds, labels, names diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index a693d150..8f5e5bd4 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -1,3 +1,4 @@ +import importlib import os import time from typing import Any, List, Optional, Tuple, Union @@ -603,18 +604,16 @@ def __init__( self.model_format = self.model_cfg["model_format"] # Init model specific functions - if self.model_format == "mmdet3d": - from detectionmetrics.models.lidar_torch_utils import mmdet3d - - self._get_sample = mmdet3d.get_sample - self._inference = mmdet3d.inference - elif "o3d" in self.model_format: - from detectionmetrics.models.lidar_torch_utils import o3d - - self._get_sample = o3d.get_sample - self._inference = o3d.inference - else: - raise raise_unknown_model_format_lidar(self.model_format) + model_format = self.model_format.split('_')[0] + model_utils_module_str = ( + f"detectionmetrics.models.lidar_torch_utils.{model_format}" + ) + try: + model_utils_module = importlib.import_module(model_utils_module_str) + except ImportError: + raise_unknown_model_format_lidar(model_format) + self._get_sample = model_utils_module.get_sample + self._inference = model_utils_module.inference def inference(self, points_fname: str) -> np.ndarray: """Perform inference for a single point cloud From c44f55452ba6b19a395c0d6d3dc267b255d13cdb Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 19 May 2025 11:17:36 +0200 Subject: [PATCH 13/39] Improve example scripts --- examples/gaia_lidar.py | 35 ++++++++++++++++++++++++++++++++++- examples/goose_lidar.py | 26 +++++++++++++++++++++++++- examples/merge_datasets.py | 18 ++++++++++++++++-- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/examples/gaia_lidar.py b/examples/gaia_lidar.py index b5e1d8a3..4280bb36 100644 --- a/examples/gaia_lidar.py +++ b/examples/gaia_lidar.py @@ -1,4 +1,5 @@ import argparse +import json from detectionmetrics.datasets.gaia import GaiaLiDARSegmentationDataset @@ -13,6 +14,23 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--dataset", type=str, required=True, help="Parquet dataset file" ) + parser.add_argument( + "--new_ontology", + type=str, + help="New ontology JSON file name", + ) + parser.add_argument( + "--ontology_translation", + type=str, + help="Ontology translation JSON file name", + ) + parser.add_argument( + "--outdir", + type=str, + required=True, + help="Directory where dataset will be stored in common format", + ) + return parser.parse_args() @@ -20,7 +38,22 @@ def main(): """Main function""" args = parse_args() - GaiaLiDARSegmentationDataset(dataset_fname=args.dataset) + new_ontology, ontology_translation = None, None + if args.new_ontology is not None: + with open(args.new_ontology, "r", encoding="utf-8") as f: + new_ontology = json.load(f) + + if args.ontology_translation is not None: + with open(args.ontology_translation, "r", encoding="utf-8") as f: + ontology_translation = json.load(f) + + dataset = GaiaLiDARSegmentationDataset(dataset_fname=args.dataset) + + dataset.export( + args.outdir, + new_ontology=new_ontology, + ontology_translation=ontology_translation, + ) if __name__ == "__main__": diff --git a/examples/goose_lidar.py b/examples/goose_lidar.py index 0ecc9693..3f860663 100644 --- a/examples/goose_lidar.py +++ b/examples/goose_lidar.py @@ -1,4 +1,5 @@ import argparse +import json from detectionmetrics.datasets.goose import GOOSELiDARSegmentationDataset @@ -26,6 +27,16 @@ def parse_args() -> argparse.Namespace: type=str, help="Directory where test dataset split is stored", ) + parser.add_argument( + "--new_ontology", + type=str, + help="New ontology JSON file name", + ) + parser.add_argument( + "--ontology_translation", + type=str, + help="Ontology translation JSON file name", + ) parser.add_argument( "--outdir", type=str, @@ -40,12 +51,25 @@ def main(): """Main function""" args = parse_args() + new_ontology, ontology_translation = None, None + if args.new_ontology is not None: + with open(args.new_ontology, "r", encoding="utf-8") as f: + new_ontology = json.load(f) + + if args.ontology_translation is not None: + with open(args.ontology_translation, "r", encoding="utf-8") as f: + ontology_translation = json.load(f) + dataset = GOOSELiDARSegmentationDataset( train_dataset_dir=args.train_dataset_dir, val_dataset_dir=args.val_dataset_dir, test_dataset_dir=args.test_dataset_dir, ) - dataset.export(args.outdir) + dataset.export( + args.outdir, + new_ontology=new_ontology, + ontology_translation=ontology_translation, + ) if __name__ == "__main__": diff --git a/examples/merge_datasets.py b/examples/merge_datasets.py index 87ce0243..cf9201a3 100644 --- a/examples/merge_datasets.py +++ b/examples/merge_datasets.py @@ -1,6 +1,6 @@ import argparse -from detectionmetrics.datasets.gaia import GaiaImageSegmentationDataset +from detectionmetrics.datasets.gaia import GaiaImageSegmentationDataset, GaiaLiDARSegmentationDataset def parse_args() -> argparse.Namespace: @@ -23,6 +23,13 @@ def parse_args() -> argparse.Namespace: required=True, help="Directory where merged dataset will be stored", ) + parser.add_argument( + "--dataset_type", + type=str, + choices=["image", "lidar"], + required=True, + help="Type of datasets to merge", + ) return parser.parse_args() @@ -31,7 +38,14 @@ def main(): """Main function""" args = parse_args() - datasets = [GaiaImageSegmentationDataset(fname) for fname in args.datasets] + if args.dataset_type == "image": + dataset_class = GaiaImageSegmentationDataset + elif args.dataset_type == "lidar": + dataset_class = GaiaLiDARSegmentationDataset + else: + raise ValueError(f"Unknown dataset type: {args.dataset_type}") + + datasets = [dataset_class(fname) for fname in args.datasets] main_dataset = datasets[0] for extra_dataset in datasets[1:]: main_dataset.append(extra_dataset) From 2f9eee3f991c1df9c99f958f5d646f393d40fcf5 Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Wed, 25 Jun 2025 16:21:24 +0200 Subject: [PATCH 14/39] Add support for WildScenes LiDAR (no intensity provided) --- detectionmetrics/datasets/dataset.py | 14 +- detectionmetrics/datasets/wildscenes.py | 187 ++++++++++++------ .../models/lidar_torch_utils/o3d/__init__.py | 5 +- detectionmetrics/utils/lidar.py | 13 +- 4 files changed, 153 insertions(+), 66 deletions(-) diff --git a/detectionmetrics/datasets/dataset.py b/detectionmetrics/datasets/dataset.py index 1b023544..31480fb1 100644 --- a/detectionmetrics/datasets/dataset.py +++ b/detectionmetrics/datasets/dataset.py @@ -164,7 +164,7 @@ def export( new_ontology=new_ontology, ontology_translation=ontology_translation, classes_to_remove=classes_to_remove, - lut_dtype=np.uint32 + lut_dtype=np.uint32, ) n_classes = max(c["idx"] for c in new_ontology.values()) + 1 else: @@ -314,6 +314,8 @@ class LiDARSegmentationDataset(SegmentationDataset): :type ontology: dict :param is_kitti_format: Whether the linked files in the dataset are stored in SemanticKITTI format or not, defaults to True :type is_kitti_format: bool, optional + :param has_intensity: Whether the point cloud files contain intensity values, defaults to True + :type has_intensity: bool, optional """ def __init__( @@ -322,9 +324,11 @@ def __init__( dataset_dir: str, ontology: dict, is_kitti_format: bool = True, + has_intensity: bool = True, ): super().__init__(dataset, dataset_dir, ontology) self.is_kitti_format = is_kitti_format + self.has_intensity = has_intensity def make_fname_global(self): """Get all relative filenames in dataset and make global""" @@ -446,8 +450,7 @@ def export( # Store dataset as Parquet file containing relative filenames self.dataset.to_parquet(os.path.join(outdir, "dataset.parquet")) - @staticmethod - def read_points(fname: str) -> np.ndarray: + def read_points(self, fname: str) -> np.ndarray: """Read point cloud. Defaults to SemanticKITTI format :param fname: File containing point cloud @@ -455,10 +458,9 @@ def read_points(fname: str) -> np.ndarray: :return: Numpy array containing points :rtype: np.ndarray """ - return ul.read_semantickitti_points(fname) + return ul.read_semantickitti_points(fname, self.has_intensity) - @staticmethod - def read_label(fname: str) -> Tuple[np.ndarray, np.ndarray]: + def read_label(self, fname: str) -> Tuple[np.ndarray, np.ndarray]: """Read semantic labels. Defaults to SemanticKITTI format :param fname: Binary file containing labels diff --git a/detectionmetrics/datasets/wildscenes.py b/detectionmetrics/datasets/wildscenes.py index cd9c0104..0e04fdba 100644 --- a/detectionmetrics/datasets/wildscenes.py +++ b/detectionmetrics/datasets/wildscenes.py @@ -8,61 +8,17 @@ from detectionmetrics.datasets import dataset as dm_dataset -# Ontology definition as found in the official repo (https://github.com/csiro-robotics/WildScenes/blob/main/wildscenes/tools/utils2d.py) -METAINFO = { - "classes": ( - "unlabelled", - "asphalt", - "dirt", - "mud", - "water", - "gravel", - "other-terrain", - "tree-trunk", - "tree-foliage", - "bush", - "fence", - "structure", - "pole", - "vehicle", - "rock", - "log", - "other-object", - "sky", - "grass", - ), - "palette": [ - (0, 0, 0), - (255, 165, 0), - (60, 180, 75), - (255, 225, 25), - (0, 130, 200), - (145, 30, 180), - (70, 240, 240), - (240, 50, 230), - (210, 245, 60), - (230, 25, 75), - (0, 128, 128), - (170, 110, 40), - (255, 250, 200), - (128, 0, 0), - (170, 255, 195), - (128, 128, 0), - (250, 190, 190), - (0, 0, 128), - (128, 128, 128), - ], - "cidx": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], -} - - -def build_dataset(dataset_dir: str, split_fnames: dict) -> Tuple[dict, dict]: +def build_dataset( + dataset_dir: str, split_fnames: dict, ontology: dict +) -> Tuple[dict, dict]: """Build dataset and ontology dictionaries from Wildscenes dataset structure :param dataset_dir: Directory where both RGB images and annotations have been extracted to :type dataset_dir: str :param split_fnames: Dictionary that contains the paths where train, val, and test split files (.csv) have been extracted to :type split_dir: str + :param ontology: Ontology definition as found in the official repo + :type ontology: dict :return: Dataset and onotology :rtype: Tuple[dict, dict] """ @@ -75,10 +31,10 @@ def build_dataset(dataset_dir: str, split_fnames: dict) -> Tuple[dict, dict]: assert os.path.isfile(split_fname), f"{split_fname} split file not found" # Load and adapt ontology - ontology = {} - ontology_iter = zip(METAINFO["classes"], METAINFO["palette"], METAINFO["cidx"]) + parsed_ontology = {} + ontology_iter = zip(ontology["classes"], ontology["palette"], ontology["cidx"]) for name, color, idx in ontology_iter: - ontology[name] = {"idx": idx, "rgb": color} + parsed_ontology[name] = {"idx": idx, "rgb": color} # Get samples filenames train_split = pd.read_csv(split_fnames["train"]) @@ -92,6 +48,9 @@ def build_dataset(dataset_dir: str, split_fnames: dict) -> Tuple[dict, dict]: samples_data = pd.concat([train_split, val_split, test_split]) + if "hist_path" in samples_data.columns: + samples_data = samples_data.drop(columns=["hist_path"]) + # Build dataset as ordered python dictionary dataset = OrderedDict() skipped_samples = [] @@ -120,18 +79,18 @@ def build_dataset(dataset_dir: str, split_fnames: dict) -> Tuple[dict, dict]: for sample_name in skipped_samples: print(f"\n\t{sample_name}") - return dataset, ontology + return dataset, parsed_ontology class WildscenesImageSegmentationDataset(dm_dataset.ImageSegmentationDataset): """Specific class for Wildscenes-styled image segmentation datasets. All data can - be downloaded from the official repo (https://github.com/unmannedlab/RELLIS-3D): + be downloaded from the official repo: dataset -> https://data.csiro.au/collection/csiro:61541 split -> https://github.com/csiro-robotics/WildScenes/tree/main/data/splits/opt2d :param dataset_dir: Directory where dataset images and labels are stored (Wildscenes2D) :type dataset_dir: str - :param split_dir: Directory where train, val, and test files (.csv) have been extracted to (data/splits/opt2d from the official repo) + :param split_dir: Directory where train, val, and test files (.csv) have been extracted to :type split_dir: str """ @@ -141,7 +100,54 @@ def __init__(self, dataset_dir: str, split_dir: str): "val": os.path.join(split_dir, "val.csv"), "test": os.path.join(split_dir, "test.csv"), } - dataset, ontology = build_dataset(dataset_dir, split_fnames) + + # Ontology definition as found in the official repo (https://github.com/csiro-robotics/WildScenes/blob/main/wildscenes/tools/utils2d.py) + METAINFO = { + "classes": ( + "unlabelled", + "asphalt", + "dirt", + "mud", + "water", + "gravel", + "other-terrain", + "tree-trunk", + "tree-foliage", + "bush", + "fence", + "structure", + "pole", + "vehicle", + "rock", + "log", + "other-object", + "sky", + "grass", + ), + "palette": [ + (0, 0, 0), + (255, 165, 0), + (60, 180, 75), + (255, 225, 25), + (0, 130, 200), + (145, 30, 180), + (70, 240, 240), + (240, 50, 230), + (210, 245, 60), + (230, 25, 75), + (0, 128, 128), + (170, 110, 40), + (255, 250, 200), + (128, 0, 0), + (170, 255, 195), + (128, 128, 0), + (250, 190, 190), + (0, 0, 128), + (128, 128, 128), + ], + "cidx": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], + } + dataset, ontology = build_dataset(dataset_dir, split_fnames, METAINFO) # Convert to Pandas cols = ["image", "label", "scene", "split"] @@ -149,3 +155,72 @@ def __init__(self, dataset_dir: str, split_dir: str): dataset.attrs = {"ontology": ontology} super().__init__(dataset, dataset_dir, ontology) + + +class WildscenesLiDARSegmentationDataset(dm_dataset.LiDARSegmentationDataset): + """Specific class for Wildscenes-styled LiDAR segmentation datasets. All data can + be downloaded from the official repo: + dataset -> https://data.csiro.au/collection/csiro:61541 + split -> https://github.com/csiro-robotics/WildScenes/tree/main/data/splits/opt3d + + :param dataset_dir: Directory where dataset images and labels are stored (Wildscenes3D) + :type dataset_dir: str + :param split_dir: Directory where train, val, and test files (.csv) have been extracted to + :type split_dir: str + """ + + def __init__(self, dataset_dir: str, split_dir: str): + split_fnames = { + "train": os.path.join(split_dir, "train.csv"), + "val": os.path.join(split_dir, "val.csv"), + "test": os.path.join(split_dir, "test.csv"), + } + + # Ontology definition as found in the official repo (https://github.com/csiro-robotics/WildScenes/blob/main/wildscenes/tools/utils3d.py) + METAINFO = { + "classes": ( + "unlabelled", + "bush", + "dirt", + "fence", + "grass", + "gravel", + "log", + "mud", + "other-object", + "other-terrain", + "rock", + "sky", + "structure", + "tree-foliage", + "tree-trunk", + "water", + ), + "palette": [ + (0, 0, 0), + (230, 25, 75), + (60, 180, 75), + (0, 128, 128), + (128, 128, 128), + (145, 30, 180), + (128, 128, 0), + (255, 225, 25), + (250, 190, 190), + (70, 240, 240), + (170, 255, 195), + (0, 0, 128), + (170, 110, 40), + (210, 245, 60), + (240, 50, 230), + (0, 130, 200), + ], + "cidx": [255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + } + dataset, ontology = build_dataset(dataset_dir, split_fnames, METAINFO) + + # Convert to Pandas + cols = ["points", "label", "scene", "split"] + dataset = pd.DataFrame.from_dict(dataset, orient="index", columns=cols) + dataset.attrs = {"ontology": ontology} + + super().__init__(dataset, dataset_dir, ontology, has_intensity=False) diff --git a/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py index 41df93e9..61060c0f 100644 --- a/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py +++ b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py @@ -96,6 +96,7 @@ def get_sample( label_fname: Optional[str] = None, name: Optional[str] = None, idx: Optional[int] = None, + has_intensity: bool = True, ) -> Tuple[np.ndarray, np.ndarray, ul.Sampler]: """Get sample data for mmdetection3d models @@ -109,10 +110,12 @@ def get_sample( :type name: Optional[str], optional :param idx: sample numerical index, defaults to None :type idx: Optional[int], optional + :param has_intensity: whether the point cloud has intensity values, defaults to True + :type has_intensity: bool, optional :return: Sample data tuple :rtype: Tuple[np.ndarray, np.ndarray, ul.Sampler, np.ndarray, str, int] """ - points = ul.read_semantickitti_points(points_fname) + points = ul.read_semantickitti_points(points_fname, has_intensity) label = None if label_fname is not None: label, _ = ul.read_semantickitti_label(label_fname) diff --git a/detectionmetrics/utils/lidar.py b/detectionmetrics/utils/lidar.py index a7699168..484956a8 100644 --- a/detectionmetrics/utils/lidar.py +++ b/detectionmetrics/utils/lidar.py @@ -282,16 +282,23 @@ def render_point_cloud( return image -def read_semantickitti_points(fname: str) -> np.ndarray: +def read_semantickitti_points(fname: str, has_intensity: bool = True) -> np.ndarray: """Read points from a binary file in SemanticKITTI format :param fname: Binary file containing points :type fname: str + :param has_intensity: Whether the points have intensity values, defaults to True + :type has_intensity: bool :return: Numpy array containing points :rtype: np.ndarray """ points = np.fromfile(fname, dtype=np.float32) - return points.reshape((-1, 4)) + points = points.reshape((-1, 4 if has_intensity else 3)) + if not has_intensity: + empty_intensity = np.zeros((points.shape[0], 1), dtype=np.float32) + points = np.concatenate([points, empty_intensity], axis=1) + return points + def read_semantickitti_label(fname: str) -> Tuple[np.ndarray, np.ndarray]: """Read labels from a binary file in SemanticKITTI format @@ -305,4 +312,4 @@ def read_semantickitti_label(fname: str) -> Tuple[np.ndarray, np.ndarray]: label = label.reshape((-1)) semantic_label = label & 0xFFFF instance_label = label >> 16 - return semantic_label, instance_label \ No newline at end of file + return semantic_label, instance_label From af8a2628885adb8bf2a5066238c4657a1ce19184 Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Wed, 25 Jun 2025 16:21:49 +0200 Subject: [PATCH 15/39] Enable origin removal (useful for RELLIS-3D) --- detectionmetrics/datasets/dataset.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/detectionmetrics/datasets/dataset.py b/detectionmetrics/datasets/dataset.py index 31480fb1..e9250f05 100644 --- a/detectionmetrics/datasets/dataset.py +++ b/detectionmetrics/datasets/dataset.py @@ -348,6 +348,7 @@ def export( ontology_translation: Optional[dict] = None, classes_to_remove: Optional[List[str]] = [], include_label_count: bool = True, + remove_origin: bool = False, ): """Export dataset dataframe and LiDAR files in SemanticKITTI format. Optionally, modify ontology before exporting. @@ -361,6 +362,8 @@ def export( :type classes_to_remove: Optional[List[str]], optional :param include_label_count: Whether to include class weights in the dataset, defaults to True :type include_label_count: bool, optional + :param remove_origin: Whether to remove the origin from the point cloud (mostly for removing RELLIS-3D spurious points), defaults to False + :type remove_origin: bool, optional """ os.makedirs(outdir, exist_ok=True) @@ -384,7 +387,7 @@ def export( # Check if label count is missing and create empty array if needed label_count_missing = include_label_count and ( - not self.has_label_count or new_ontology is not None + not self.has_label_count or new_ontology is not None or remove_origin ) if label_count_missing: label_count = np.zeros(n_classes, dtype=np.uint64) @@ -416,17 +419,26 @@ def export( not self.is_kitti_format or ontology_conversion_lut is not None or label_count_missing + or remove_origin ): points = self.read_points(points_fname) label = self.read_label(label_fname) + + # Convert label to new ontology if needed if ontology_conversion_lut is not None: label = ontology_conversion_lut[label].astype(np.uint32) + + # Remove points in coordinate origin if needed + if remove_origin: + mask = np.all(points[:, :3] != 0, axis=1) + points = points[mask] + label = label[mask] + points.tofile(os.path.join(outdir, rel_points_fname)) label.tofile(os.path.join(outdir, rel_label_fname)) - if label_count_missing: - indices, counts = np.unique(label, return_counts=True) - label_count[indices] += counts.astype(np.uint64) + indices, counts = np.unique(label, return_counts=True) + label_count[indices] += counts.astype(np.uint64) else: shutil.copy2(points_fname, os.path.join(outdir, rel_points_fname)) shutil.copy2(label_fname, os.path.join(outdir, rel_label_fname)) From b1d470d6183c1a5527fff3f80916558cb3da98b7 Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Wed, 25 Jun 2025 16:22:10 +0200 Subject: [PATCH 16/39] Add GOOSE Ex support --- detectionmetrics/datasets/goose.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/detectionmetrics/datasets/goose.py b/detectionmetrics/datasets/goose.py index 0278d5f7..5f117fa3 100644 --- a/detectionmetrics/datasets/goose.py +++ b/detectionmetrics/datasets/goose.py @@ -16,6 +16,7 @@ def build_dataset( train_dataset_dir: Optional[str] = None, val_dataset_dir: Optional[str] = None, test_dataset_dir: Optional[str] = None, + is_goose_ex: bool = False, ) -> Tuple[dict, dict]: """Build dataset and ontology dictionaries from GOOSE dataset structure @@ -31,6 +32,8 @@ def build_dataset( :type val_dataset_dir: str, optional :param test_dataset_dir: Directory containing test data, defaults to None :type test_dataset_dir: str, optional + :param is_goose_ex: Whether the dataset is GOOSE Ex or GOOSE, defaults to False + :type is_goose_ex: bool, optional :return: Dataset and onotology :rtype: Tuple[dict, dict] """ @@ -66,13 +69,23 @@ def build_dataset( train_data = os.path.join(dataset_dir, f"{data_type}/{split}/*/*_{data_suffix}") for data_fname in glob(train_data): sample_dir, sample_base_name = os.path.split(data_fname) - sample_base_name = sample_base_name.split("__")[-1] + + # GOOSE Ex uses a different label file naming convention + if is_goose_ex: + sample_base_name = "sequence" + sample_base_name.split("_sequence")[-1] + else: + sample_base_name = sample_base_name.split("__")[-1] + sample_base_name = sample_base_name.split("_" + data_suffix)[0] scene = os.path.split(sample_dir)[-1] sample_name = f"{scene}-{sample_base_name}" - label_base_name = f"{scene}__{sample_base_name}_{label_suffix}" + if is_goose_ex: + label_base_name = f"{scene}_{sample_base_name}_{label_suffix}" + else: + label_base_name = f"{scene}__{sample_base_name}_{label_suffix}" + label_fname = os.path.join( dataset_dir, "labels", split, scene, label_base_name ) @@ -131,9 +144,9 @@ def __init__( class GOOSELiDARSegmentationDataset(dm_dataset.LiDARSegmentationDataset): """Specific class for GOOSE-styled LiDAR segmentation datasets. All data can be downloaded from the official webpage (https://goose-dataset.de): - train -> https://goose-dataset.de/storage/goose_3d_train.zip - val -> https://goose-dataset.de/storage/goose_3d_val.zip - test -> https://goose-dataset.de/storage/goose_3d_test.zip + train -> https://goose-dataset.de/storage/gooseEx_3d_train.zip + val -> https://goose-dataset.de/storage/gooseEx_3d_val.zip + test -> https://goose-dataset.de/storage/gooseEx_3d_test.zip :param train_dataset_dir: Directory containing training data :type train_dataset_dir: str @@ -141,6 +154,8 @@ class GOOSELiDARSegmentationDataset(dm_dataset.LiDARSegmentationDataset): :type val_dataset_dir: str, optional :param test_dataset_dir: Directory containing test data, defaults to None :type test_dataset_dir: str, optional + :param is_goose_ex: Whether the dataset is GOOSE Ex or GOOSE, defaults to False + :type is_goose_ex: bool, optional """ def __init__( @@ -148,14 +163,16 @@ def __init__( train_dataset_dir: Optional[str] = None, val_dataset_dir: Optional[str] = None, test_dataset_dir: Optional[str] = None, + is_goose_ex: bool = False, ): dataset, ontology = build_dataset( "lidar", - "vls128.bin", + "pcl.bin" if is_goose_ex else "vls128.bin", "goose.label", train_dataset_dir, val_dataset_dir, test_dataset_dir, + is_goose_ex=is_goose_ex, ) # Convert to Pandas From 410b1933f4dd1ee6041e77d23f985d2a1feceac9 Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Wed, 25 Jun 2025 16:22:22 +0200 Subject: [PATCH 17/39] Update models --- .../models/lidar_torch_utils/lsk3dnet.py | 13 +++++++------ .../models/lidar_torch_utils/mmdet3d.py | 8 +++++++- .../models/lidar_torch_utils/sphereformer.py | 12 ++++++------ detectionmetrics/models/torch.py | 15 ++++++++++++--- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py index 5e49a67c..ee1cbaa4 100644 --- a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py +++ b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py @@ -6,6 +6,7 @@ import utils.depth_map_utils as depth_map_utils import detectionmetrics.utils.torch as ut +import detectionmetrics.utils.lidar as ul def range_projection(current_vertex, fov_up=3.0, fov_down=-25.0, proj_H=64, proj_W=900): @@ -142,6 +143,7 @@ def get_sample( label_fname: Optional[str] = None, name: Optional[str] = None, idx: Optional[int] = None, + has_intensity: bool = True, ) -> dict: """Get sample data for mmdetection3d models @@ -155,18 +157,17 @@ def get_sample( :type name: Optional[str], optional :param idx: sample numerical index, defaults to None :type idx: Optional[int], optional + :param has_intensity: whether the point cloud has intensity values, defaults to True + :type has_intensity: bool, optional :return: Sample data dictionary :rtype: dict """ - raw_data = np.fromfile(points_fname, dtype=np.float32) - raw_data = raw_data.reshape((-1, 4)) + raw_data = ul.read_semantickitti_points(points_fname, has_intensity) labels, ref_labels = None, None if label_fname is not None: - annotated_data = np.fromfile(label_fname, dtype=np.uint32) - annotated_data = annotated_data.reshape((-1, 1)) - labels = annotated_data & 0xFFFF # delete high 16 digits binary - labels = labels.astype(np.uint8) + labels = ul.read_semantickitti_label(label_fname) + labels = labels.reshape((-1, 1)).astype(np.uint8) ref_labels = labels.copy() xyz = raw_data[:, :3] diff --git a/detectionmetrics/models/lidar_torch_utils/mmdet3d.py b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py index 8fb0772d..a003089a 100644 --- a/detectionmetrics/models/lidar_torch_utils/mmdet3d.py +++ b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py @@ -18,6 +18,7 @@ def get_sample( label_fname: Optional[str] = None, name: Optional[str] = None, idx: Optional[int] = None, + has_intensity: bool = True, ) -> dict: """Get sample data for mmdetection3d models @@ -31,6 +32,8 @@ def get_sample( :type name: Optional[str], optional :param idx: sample numerical index, defaults to None :type idx: Optional[int], optional + :param has_intensity: whether the point cloud has intensity values, defaults to True + :type has_intensity: bool, optional :return: Sample data dictionary :rtype: dict """ @@ -47,7 +50,10 @@ def get_sample( } n_feats = sample["num_pts_feats"] - transforms = [LoadPointsFromFile(coord_type="LIDAR", load_dim=4, use_dim=n_feats)] + load_dim = 4 if has_intensity else 3 + transforms = [ + LoadPointsFromFile(coord_type="LIDAR", load_dim=load_dim, use_dim=n_feats) + ] if sample["pts_semantic_mask_path"] is not None: transforms.append( LoadAnnotations3D( diff --git a/detectionmetrics/models/lidar_torch_utils/sphereformer.py b/detectionmetrics/models/lidar_torch_utils/sphereformer.py index a92adcd9..67e93df6 100644 --- a/detectionmetrics/models/lidar_torch_utils/sphereformer.py +++ b/detectionmetrics/models/lidar_torch_utils/sphereformer.py @@ -6,6 +6,7 @@ from util.data_util import data_prepare import detectionmetrics.utils.torch as ut +import detectionmetrics.utils.lidar as ul def collate_fn(samples: List[dict]) -> dict: @@ -51,6 +52,7 @@ def get_sample( label_fname: Optional[str] = None, name: Optional[str] = None, idx: Optional[int] = None, + has_intensity: bool = True, ) -> dict: """Get sample data for mmdetection3d models @@ -64,18 +66,16 @@ def get_sample( :type name: Optional[str], optional :param idx: sample numerical index, defaults to None :type idx: Optional[int], optional + :param has_intensity: whether the point cloud has intensity values, defaults to True + :type has_intensity: bool, optional :return: Sample data dictionary :rtype: dict """ - feats = np.fromfile(points_fname, dtype=np.float32) - feats = feats.reshape((-1, 4))[:, : model_cfg["n_feats"]] + feats = ul.read_semantickitti_points(points_fname, has_intensity) + feats = feats[:, : model_cfg["n_feats"]] annotated_data = np.fromfile(label_fname, dtype=np.uint32) annotated_data = annotated_data.reshape((-1, 1)) - labels = annotated_data & 0xFFFF # delete high 16 digits binary - labels = labels.astype(np.uint8) - instance_label = annotated_data >> 16 - instance_label = instance_label.reshape(-1) xyz = feats[:, :3] diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index 8f5e5bd4..a85ae4e1 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -260,6 +260,7 @@ def __getitem__(self, idx: int): label_fname=self.dataset.dataset.iloc[idx]["label"], name=self.dataset.dataset.index[idx], idx=idx, + has_intensity=self.dataset.has_intensity, ) @@ -604,7 +605,7 @@ def __init__( self.model_format = self.model_cfg["model_format"] # Init model specific functions - model_format = self.model_format.split('_')[0] + model_format = self.model_format.split("_")[0] model_utils_module_str = ( f"detectionmetrics.models.lidar_torch_utils.{model_format}" ) @@ -615,16 +616,24 @@ def __init__( self._get_sample = model_utils_module.get_sample self._inference = model_utils_module.inference - def inference(self, points_fname: str) -> np.ndarray: + def inference( + self, + points_fname: str, + has_intensity: bool = True, + ) -> np.ndarray: """Perform inference for a single point cloud :param points_fname: Point cloud in SemanticKITTI .bin format :type points_fname: str + :param has_intensity: Whether the point cloud has intensity values, defaults to True + :type has_intensity: bool, optional :return: Segmenation result as a point cloud with label indices :rtype: np.ndarray """ # Preprocess point cloud - sample = self._get_sample(points_fname, self.model_cfg) + sample = self._get_sample( + points_fname, self.model_cfg, has_intensity=has_intensity + ) pred, _, _ = self._inference(sample, self.model, self.model_cfg) return pred.squeeze().cpu().numpy() From fe1c5aa060dbf7e21dc8fbca8bfefcb81fcf65df Mon Sep 17 00:00:00 2001 From: "d.pascualhe" Date: Wed, 25 Jun 2025 16:22:49 +0200 Subject: [PATCH 18/39] Upgrade utils for rendering LiDAR --- detectionmetrics/utils/lidar.py | 48 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/detectionmetrics/utils/lidar.py b/detectionmetrics/utils/lidar.py index 484956a8..3d2b4226 100644 --- a/detectionmetrics/utils/lidar.py +++ b/detectionmetrics/utils/lidar.py @@ -1,6 +1,6 @@ import numpy as np import random -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union import open3d as o3d from PIL import Image @@ -13,7 +13,21 @@ "front": np.array([1, 0, 0.5], dtype=np.float32), # Camera front vector "lookat": np.array([1, 0.0, 0.0], dtype=np.float32), # Point camera looks at "up": np.array([-0.5, 0, 1], dtype=np.float32), # Camera up direction - } + }, + "top": { + "zoom": 0.025, + "front": np.array([0, 0, -1], dtype=np.float32), # Looking straight down + "lookat": np.array([1, 0.0, 0.0], dtype=np.float32), # Same target point + "up": np.array([0, 1, 0], dtype=np.float32), # Y axis is "up" in image + }, + "side": { + "zoom": 0.012, + "front": np.array( + [0, -1, 0], dtype=np.float32 + ), # Looking from positive Y toward origin + "lookat": np.array([1, 0.0, 0.0], dtype=np.float32), # Same target point + "up": np.array([0, 0, 1], dtype=np.float32), # Z axis is up + }, } @@ -211,11 +225,13 @@ def view_point_cloud(points: np.ndarray, colors: np.ndarray): def render_point_cloud( points: np.ndarray, colors: np.ndarray, - camera_view: str = "3rd_person", + camera_view: Union[str, dict] = "3rd_person", bg_color: Optional[List[float]] = [0.0, 0.0, 0.0, 1.0], color_jitter: float = 0.05, point_size: float = 3.0, resolution: Tuple[int, int] = (1920, 1080), + render_origin: bool = False, + origin_size: float = 0.5, ) -> Image: """Render a given point cloud from a specific camera view and return the image @@ -223,8 +239,8 @@ def render_point_cloud( :type points: np.ndarray :param colors: Colors for the point cloud data :type colors: np.ndarray - :param camera_view: Camera view, defaults to "3rd_person" - :type camera_view: str, optional + :param camera_view: Camera view (either ID or dictionary containing camera definition), defaults to "3rd_person" + :type camera_view: Union[str, dict], optional :param bg_color: Background color, defaults to black -> [0., 0., 0., 1.] :type bg_color: Optional[List[float]], optional :param color_jitter: Jitters the colors by a random value between [-color_jitter, color_jitter], defaults to 0.05 @@ -233,11 +249,20 @@ def render_point_cloud( :type point_size: float, optional :param resolution: Render resolution, defaults to (1920, 1080) :type resolution: Tuple[int, int], optional + :param render_origin: Whether to render the origin axes, defaults to False + :type render_origin: bool, optional + :param origin_size: Size of the origin axes, defaults to 0.5 + :type origin_size: float, optional :return: Rendered point cloud :rtype: Image """ - assert camera_view in CAMERA_VIEWS, f"Camera view {camera_view} not implemented" - view_settings = CAMERA_VIEWS[camera_view] + if isinstance(camera_view, dict): + # If camera_view is a dictionary, use it directly + view_settings = camera_view + elif isinstance(camera_view, str): + # If camera_view is a string, look it up in predefined views + assert camera_view in CAMERA_VIEWS, f"Camera view {camera_view} not implemented" + view_settings = CAMERA_VIEWS[camera_view] # Add color jitter if needed if color_jitter > 0: @@ -258,6 +283,15 @@ def render_point_cloud( material.point_size = point_size renderer.scene.add_geometry("point_cloud", point_cloud, material) + # Add origin axes for reference + if render_origin: + coord_frame = o3d.geometry.TriangleMesh.create_coordinate_frame( + size=origin_size, origin=[0, 0, 0] + ) + coord_material = o3d.visualization.rendering.MaterialRecord() + coord_material.shader = "defaultUnlit" # Also unlit for visibility + renderer.scene.add_geometry("coordinate_frame", coord_frame, coord_material) + # Set the background color renderer.scene.set_background(bg_color) From 8ebb163dfa827e12e7a8e257fd5d5bb864798a69 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 3 Sep 2025 11:21:54 +0200 Subject: [PATCH 19/39] Fixes in data parsing for LSK3DNet and SphereFormer --- detectionmetrics/models/lidar_torch_utils/lsk3dnet.py | 2 +- .../models/lidar_torch_utils/sphereformer.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py index ee1cbaa4..14a85b16 100644 --- a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py +++ b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py @@ -166,7 +166,7 @@ def get_sample( labels, ref_labels = None, None if label_fname is not None: - labels = ul.read_semantickitti_label(label_fname) + labels, _ = ul.read_semantickitti_label(label_fname) labels = labels.reshape((-1, 1)).astype(np.uint8) ref_labels = labels.copy() diff --git a/detectionmetrics/models/lidar_torch_utils/sphereformer.py b/detectionmetrics/models/lidar_torch_utils/sphereformer.py index 67e93df6..92f0554e 100644 --- a/detectionmetrics/models/lidar_torch_utils/sphereformer.py +++ b/detectionmetrics/models/lidar_torch_utils/sphereformer.py @@ -30,7 +30,8 @@ def collate_fn(samples: List[dict]) -> dict: coords = torch.cat(coords) xyz = torch.cat(xyz) feats = torch.cat(feats) - labels = torch.cat(labels) + if any(label is None for label in labels): + labels = None offset = torch.IntTensor(offset) inds_recons = torch.cat(inds_recons) @@ -74,14 +75,16 @@ def get_sample( feats = ul.read_semantickitti_points(points_fname, has_intensity) feats = feats[:, : model_cfg["n_feats"]] - annotated_data = np.fromfile(label_fname, dtype=np.uint32) - annotated_data = annotated_data.reshape((-1, 1)) + labels_in = None + if label_fname is not None: + annotated_data = np.fromfile(label_fname, dtype=np.uint32) + annotated_data = annotated_data.reshape((-1, 1)) + labels_in = annotated_data.astype(np.uint8).reshape(-1) xyz = feats[:, :3] xyz = np.clip(xyz, model_cfg["pc_range"][0], model_cfg["pc_range"][1]) - labels_in = annotated_data.astype(np.uint8).reshape(-1) coords, xyz, feats, labels, inds_reconstruct = data_prepare( xyz, From 40fac6204afca20e301303cb019b4f7a0db6b67b Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 3 Sep 2025 11:22:08 +0200 Subject: [PATCH 20/39] Fix in image dataset ontology conversion --- detectionmetrics/datasets/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectionmetrics/datasets/dataset.py b/detectionmetrics/datasets/dataset.py index e9250f05..1755cbe8 100644 --- a/detectionmetrics/datasets/dataset.py +++ b/detectionmetrics/datasets/dataset.py @@ -226,7 +226,7 @@ def export( # Convert label to new ontology if needed if ontology_conversion_lut is not None: - label = ontology_conversion_lut[label] + label = ontology_conversion_lut[label].astype(np.uint8) # Resize label if needed if resize is not None: From 0470f960ffacf9bef79a3febdc5ab55d87d75ea8 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 10 Sep 2025 16:55:03 +0200 Subject: [PATCH 21/39] Add image size to model parameters in batch commands --- detectionmetrics/cli/batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectionmetrics/cli/batch.py b/detectionmetrics/cli/batch.py index 0dae173a..bb4e249a 100644 --- a/detectionmetrics/cli/batch.py +++ b/detectionmetrics/cli/batch.py @@ -102,7 +102,7 @@ def batch(command, jobs_cfg): "model": model_cfg["path"], "model_ontology": model_cfg["ontology"], "model_cfg": model_cfg["cfg"], - # "image_size": model_cfg.get("image_size", None), + "image_size": model_cfg.get("image_size", None), } ) if has_dataset: From b26294ea71092ffec7754f1dbb33c66f9e070a25 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Wed, 10 Sep 2025 16:55:43 +0200 Subject: [PATCH 22/39] Improve GAIA image example --- examples/gaia_image.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/examples/gaia_image.py b/examples/gaia_image.py index 1718aca1..4a8f3a88 100644 --- a/examples/gaia_image.py +++ b/examples/gaia_image.py @@ -1,4 +1,5 @@ import argparse +import json from detectionmetrics.datasets.gaia import GaiaImageSegmentationDataset @@ -13,6 +14,16 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--dataset", type=str, required=True, help="Parquet dataset file" ) + parser.add_argument( + "--new_ontology", + type=str, + help="New ontology JSON file name", + ) + parser.add_argument( + "--ontology_translation", + type=str, + help="Ontology translation JSON file name", + ) parser.add_argument( "--outdir", type=str, @@ -41,11 +52,26 @@ def main(): """Main function""" args = parse_args() + new_ontology, ontology_translation = None, None + if args.new_ontology is not None: + with open(args.new_ontology, "r", encoding="utf-8") as f: + new_ontology = json.load(f) + + if args.ontology_translation is not None: + with open(args.ontology_translation, "r", encoding="utf-8") as f: + ontology_translation = json.load(f) + dataset = GaiaImageSegmentationDataset(dataset_fname=args.dataset) if args.split: dataset.dataset = dataset.dataset[dataset.dataset["split"] == args.split] dataset.has_label_count = False - dataset.export(outdir=args.outdir, resize=args.resize) + + dataset.export( + outdir=args.outdir, + resize=args.resize, + new_ontology=new_ontology, + ontology_translation=ontology_translation, + ) if __name__ == "__main__": From 2bebb1803afe10c6c3dfc328688c04f9cdc8fd86 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Sep 2025 18:15:52 +0200 Subject: [PATCH 23/39] Remove unused tensorflow_explicit model format --- detectionmetrics/cli/computational_cost.py | 3 +-- docs/_pages/v2/usage.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/detectionmetrics/cli/computational_cost.py b/detectionmetrics/cli/computational_cost.py index 951f8a47..284c6285 100644 --- a/detectionmetrics/cli/computational_cost.py +++ b/detectionmetrics/cli/computational_cost.py @@ -12,8 +12,7 @@ # model @click.option( "--model_format", - type=click.Choice( - ["torch", "tensorflow", "tensorflow_explicit"], case_sensitive=False + type=click.Choice(["torch", "tensorflow"], case_sensitive=False ), show_default=True, default="torch", diff --git a/docs/_pages/v2/usage.md b/docs/_pages/v2/usage.md index 52cabaf1..aeb0562c 100644 --- a/docs/_pages/v2/usage.md +++ b/docs/_pages/v2/usage.md @@ -32,7 +32,7 @@ Usage: dm_evaluate [OPTIONS] {segmentation} {image|lidar} Evaluate model on dataset Options: - --model_format [torch|tensorflow|tensorflow_explicit] + --model_format [torch|tensorflow] Trained model format [default: torch] --model PATH Trained model filename (TorchScript) or directory (TensorFlow SavedModel) From aaa18cc6eeac6d1606862bbdaa1127ba621d7c9b Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Sep 2025 18:16:31 +0200 Subject: [PATCH 24/39] Remove unused tensorflow_explicit model format --- detectionmetrics/cli/evaluate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/detectionmetrics/cli/evaluate.py b/detectionmetrics/cli/evaluate.py index 14839c98..ef9641e0 100644 --- a/detectionmetrics/cli/evaluate.py +++ b/detectionmetrics/cli/evaluate.py @@ -25,7 +25,7 @@ def parse_split(ctx, param, value): @click.option( "--model_format", type=click.Choice( - ["torch", "tensorflow", "tensorflow_explicit"], case_sensitive=False + ["torch", "tensorflow"], case_sensitive=False ), show_default=True, default="torch", @@ -197,3 +197,7 @@ def evaluate( results.to_csv(out_fname) return results + + +if __name__ == "__main__": + evaluate() From ed4a39ac9e092ea1c7feddd32fae91913fbd44d8 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Sep 2025 18:17:19 +0200 Subject: [PATCH 25/39] Raise error if dataset samples are exported to an existing location --- detectionmetrics/datasets/dataset.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/detectionmetrics/datasets/dataset.py b/detectionmetrics/datasets/dataset.py index 1755cbe8..ab0770f0 100644 --- a/detectionmetrics/datasets/dataset.py +++ b/detectionmetrics/datasets/dataset.py @@ -440,8 +440,13 @@ def export( indices, counts = np.unique(label, return_counts=True) label_count[indices] += counts.astype(np.uint64) else: - shutil.copy2(points_fname, os.path.join(outdir, rel_points_fname)) - shutil.copy2(label_fname, os.path.join(outdir, rel_label_fname)) + new_points_fname = os.path.join(outdir, rel_points_fname) + new_label_fname = os.path.join(outdir, rel_label_fname) + try: + shutil.copy2(points_fname, new_points_fname) + shutil.copy2(label_fname, new_label_fname) + except shutil.SameFileError: + pass # Source and destination are the same file self.dataset.at[sample_name, "points"] = rel_points_fname self.dataset.at[sample_name, "label"] = rel_label_fname From 131b65a5147cc5ca7cce60b4cc7e4fbb061bd994 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Sep 2025 18:20:02 +0200 Subject: [PATCH 26/39] Split inference into predict and inference for segmentation models --- detectionmetrics/models/model.py | 30 +++++++---- detectionmetrics/models/tensorflow.py | 36 +++++++------ detectionmetrics/models/torch.py | 63 ++++++++++------------ examples/store_image_video.py | 2 +- examples/store_lidar_video.py | 2 +- examples/tensorflow_image.py | 2 +- examples/torch_image.py | 2 +- examples/torch_lidar.py | 2 +- examples/torch_native_image.py | 2 +- examples/tutorial_image_segmentation.ipynb | 4 +- 10 files changed, 75 insertions(+), 70 deletions(-) diff --git a/detectionmetrics/models/model.py b/detectionmetrics/models/model.py index 07d7bf4d..4439e3b9 100644 --- a/detectionmetrics/models/model.py +++ b/detectionmetrics/models/model.py @@ -52,18 +52,27 @@ def __init__( self.model_cfg["n_classes"] = self.n_classes @abstractmethod - def inference( - self, points: Union[np.ndarray, Image.Image] - ) -> Union[np.ndarray, Image.Image]: - """Perform inference for a single image or point cloud - - :param image: Either a numpy array (LiDAR point cloud) or a PIL image - :type image: Union[np.ndarray, Image.Image] - :return: Segmenation result as a point cloud or image with label indices + def predict(self, data: Union[np.ndarray, Image.Image]) -> Union[np.ndarray, Image.Image]: + """Perform prediction for a single data sample + + :param data: Input data sample (image or point cloud) + :type data: Union[np.ndarray, Image.Image] + :return: Prediction result :rtype: Union[np.ndarray, Image.Image] """ raise NotImplementedError + @abstractmethod + def predict(self, tensor_in): + """Perform inference for a tensor + + :param tensor_in: Input tensor (image or point cloud) + :type tensor_in: Either tf.Tensor or torch.Tensor + :return: Segmenation result as a tensor + :rtype: Either tf.Tensor or torch.Tensor + """ + raise NotImplementedError + @abstractmethod def eval( self, @@ -139,7 +148,7 @@ def __init__( super().__init__(model, model_type, model_cfg, ontology_fname, model_fname) @abstractmethod - def inference(self, image: Image.Image) -> Image.Image: + def predict(self, image: Image.Image) -> Image.Image: """Perform inference for a single image :param image: PIL image. @@ -194,6 +203,7 @@ def get_computational_cost( """ raise NotImplementedError + class LiDARSegmentationModel(SegmentationModel): """Parent LiDAR segmentation model class @@ -220,7 +230,7 @@ def __init__( super().__init__(model, model_type, model_cfg, ontology_fname, model_fname) @abstractmethod - def inference(self, points_fname: np.ndarray) -> np.ndarray: + def predict(self, points_fname: np.ndarray) -> np.ndarray: """Perform inference for a single point cloud :param points_fname: Point cloud in SemanticKITTI .bin format diff --git a/detectionmetrics/models/tensorflow.py b/detectionmetrics/models/tensorflow.py index 5679ccee..6207879f 100644 --- a/detectionmetrics/models/tensorflow.py +++ b/detectionmetrics/models/tensorflow.py @@ -361,27 +361,37 @@ def t_in(image): tf.argmax(tf.squeeze(x), axis=2).numpy().astype(np.uint8) ) - def inference(self, image: Image.Image) -> Image.Image: - """Perform inference for a single image + def predict(self, image: Image.Image) -> Image.Image: + """Perform prediction for a single image :param image: PIL image :type image: Image.Image - :return: segmenation result as PIL image + :return: Segmentation result as PIL image :rtype: Image.Image """ tensor = self.t_in(image) + result = self.predict(tensor) + return self.t_out(result) + + def predict(self, tensor_in: tf.Tensor) -> tf.Tensor: + """Perform inference for a tensor + :param tensor_in: Input point cloud tensor + :type tensor_in: tf.Tensor + :return: Segmentation result as tensor + :rtype: tf.Tensor + """ if self.model_type == "native": - result = self.model(tensor) + tensor_out = self.model(tensor_in, training=False) elif self.model_type == "compiled": - result = self.model.signatures["serving_default"](tensor) + tensor_out = self.model.signatures["serving_default"](tensor_in) else: raise ValueError("Model type not recognized") - if isinstance(result, dict): - result = list(result.values())[0] + if isinstance(tensor_out, dict): + tensor_out = list(tensor_out.values())[0] - return self.t_out(result) + return tensor_out def eval( self, @@ -445,15 +455,7 @@ def eval( for idx, image, label in pbar: idx = idx.numpy() - if self.model_type == "native": - pred = self.model(image, training=False) - elif self.model_type == "compiled": - pred = self.model.signatures["serving_default"](image) - else: - raise ValueError("Model type not recognized") - - if isinstance(pred, dict): - pred = list(pred.values())[0] + pred = self.predict(image) # Get valid points masks depending on ignored label indices if ignored_label_indices: diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index a85ae4e1..768997e8 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -374,38 +374,48 @@ def __init__( ] ) - def inference(self, image: Image.Image) -> Image.Image: - """Perform inference for a single image + def predict(self, image: Image.Image) -> Image.Image: + """Perform prediction for a single image :param image: PIL image :type image: Image.Image - :return: segmenation result as PIL image + :return: Segmentation result as a PIL image :rtype: Image.Image """ tensor = self.transform_input(image).unsqueeze(0).to(self.device) + result = self.predict(tensor) + return self.transform_output(result) + + def predict(self, tensor_in: torch.Tensor) -> torch.Tensor: + """Perform inference for a tensor + :param tensor_in: Input point cloud tensor + :type tensor_in: torch.Tensor + :return: Segmentation result as tensor + :rtype: torch.Tensor + """ with torch.no_grad(): # Perform inference if hasattr(self.model, "inference"): # e.g. mmsegmentation models - result = self.model.inference( - tensor.to(self.device), + tensor_out = self.model.inference( + tensor_in.to(self.device), [ dict( - ori_shape=tensor.shape[2:], - img_shape=tensor.shape[2:], - pad_shape=tensor.shape[2:], + ori_shape=tensor_in.shape[2:], + img_shape=tensor_in.shape[2:], + pad_shape=tensor_in.shape[2:], padding_size=[0, 0, 0, 0], ) ] - * tensor.shape[0], + * tensor_in.shape[0], ) else: - result = self.model(tensor.to(self.device)) + tensor_out = self.model(tensor_in.to(self.device)) - if isinstance(result, dict): - result = result["out"] + if isinstance(tensor_out, dict): + tensor_out = tensor_out["out"] - return self.transform_output(result) + return tensor_out def eval( self, @@ -471,24 +481,7 @@ def eval( pbar = tqdm(dataloader, leave=True) for idx, image, label in pbar: # Perform inference - if hasattr(self.model, "inference"): # e.g. mmsegmentation models - pred = self.model.inference( - image.to(self.device), - [ - dict( - ori_shape=image.shape[2:], - img_shape=image.shape[2:], - pad_shape=image.shape[2:], - padding_size=[0, 0, 0, 0], - ) - ] - * image.shape[0], - ) - else: - pred = self.model(image.to(self.device)) - - if isinstance(pred, dict): - pred = pred["out"] + pred = self.predict(image) # Get valid points masks depending on ignored label indices if ignored_label_indices: @@ -616,12 +609,12 @@ def __init__( self._get_sample = model_utils_module.get_sample self._inference = model_utils_module.inference - def inference( + def predict( self, points_fname: str, has_intensity: bool = True, ) -> np.ndarray: - """Perform inference for a single point cloud + """Perform prediction for a single point cloud :param points_fname: Point cloud in SemanticKITTI .bin format :type points_fname: str @@ -634,7 +627,7 @@ def inference( sample = self._get_sample( points_fname, self.model_cfg, has_intensity=has_intensity ) - pred, _, _ = self._inference(sample, self.model, self.model_cfg) + pred, _, _ = self.inference(sample, self.model, self.model_cfg) return pred.squeeze().cpu().numpy() @@ -696,7 +689,7 @@ def eval( pbar = tqdm(dataset, total=len(dataset), leave=True) for sample in pbar: # Perform inference - pred, label, name = self._inference(sample, self.model, self.model_cfg) + pred, label, name = self.inference(sample, self.model, self.model_cfg) # Get valid points masks depending on ignored label indices if ignored_label_indices: diff --git a/examples/store_image_video.py b/examples/store_image_video.py index 7e25d52e..20ab3694 100644 --- a/examples/store_image_video.py +++ b/examples/store_image_video.py @@ -104,7 +104,7 @@ def main(): if model is not None: image = Image.open(sample_data["image"]) - label = model.inference(image) + label = model.predict(image) lut = uc.ontology_to_rgb_lut(model.ontology) else: label = Image.open(sample_data["label"]) diff --git a/examples/store_lidar_video.py b/examples/store_lidar_video.py index c8ce31d2..d2010f82 100644 --- a/examples/store_lidar_video.py +++ b/examples/store_lidar_video.py @@ -112,7 +112,7 @@ def main(): point_cloud = dataset.read_points(sample_data["points"]) if model is not None: - label = model.inference(point_cloud) + label = model.predict(point_cloud) lut = uc.ontology_to_rgb_lut(model.ontology) else: label = dataset.read_label(sample_data["label"]) diff --git a/examples/tensorflow_image.py b/examples/tensorflow_image.py index 058c2928..024910ec 100644 --- a/examples/tensorflow_image.py +++ b/examples/tensorflow_image.py @@ -73,7 +73,7 @@ def main(): if args.image is not None: image = Image.open(args.image).convert("RGB") - result = model.inference(image) + result = model.predict(image) result = uc.label_to_rgb(result, model.ontology) result.show() diff --git a/examples/torch_image.py b/examples/torch_image.py index 2f0e7e38..2f68a28b 100644 --- a/examples/torch_image.py +++ b/examples/torch_image.py @@ -73,7 +73,7 @@ def main(): if args.image is not None: image = Image.open(args.image).convert("RGB") - result = model.inference(image) + result = model.predict(image) result = uc.label_to_rgb(result, model.ontology) result.show() diff --git a/examples/torch_lidar.py b/examples/torch_lidar.py index 65aa1085..9ed666d8 100644 --- a/examples/torch_lidar.py +++ b/examples/torch_lidar.py @@ -75,7 +75,7 @@ def main(): dataset = GaiaLiDARSegmentationDataset(args.dataset) if args.point_cloud is not None: - result = model.inference(args.point_cloud) + result = model.predict(args.point_cloud) lut = uc.ontology_to_rgb_lut(model.ontology) colors = lut[result] / 255.0 point_cloud = dataset.read_points(args.point_cloud) diff --git a/examples/torch_native_image.py b/examples/torch_native_image.py index ab590098..f74c64c7 100644 --- a/examples/torch_native_image.py +++ b/examples/torch_native_image.py @@ -79,7 +79,7 @@ def main(): if args.image is not None: image = Image.open(args.image).convert("RGB") - result = model.inference(image) + result = model.predict(image) result = uc.label_to_rgb(result, model.ontology) result.show() diff --git a/examples/tutorial_image_segmentation.ipynb b/examples/tutorial_image_segmentation.ipynb index 654f402e..9bfff7c1 100644 --- a/examples/tutorial_image_segmentation.ipynb +++ b/examples/tutorial_image_segmentation.ipynb @@ -130,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -158,7 +158,7 @@ "label = Image.open(label_fname)\n", "label = uc.label_to_rgb(label, dataset.ontology)\n", "\n", - "pred = model.inference(image)\n", + "pred = model.predict(image)\n", "pred = uc.label_to_rgb(pred, model.ontology)\n", "pred = pred.resize(label.size)\n", "\n", From 26917dd221c0b77d5741516422bf1448af01c1d4 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Sep 2025 18:20:22 +0200 Subject: [PATCH 27/39] Enable computational cost estimation for LiDAR segmentation --- detectionmetrics/cli/batch.py | 32 +++- detectionmetrics/cli/computational_cost.py | 68 +++++-- .../models/lidar_torch_utils/lsk3dnet.py | 39 +++- .../models/lidar_torch_utils/mmdet3d.py | 52 ++++- .../models/lidar_torch_utils/o3d/__init__.py | 89 ++++++++- .../models/lidar_torch_utils/sphereformer.py | 52 ++++- detectionmetrics/models/tensorflow.py | 110 ++++------- detectionmetrics/models/torch.py | 181 ++++++++++-------- 8 files changed, 428 insertions(+), 195 deletions(-) diff --git a/detectionmetrics/cli/batch.py b/detectionmetrics/cli/batch.py index bb4e249a..a9ebd323 100644 --- a/detectionmetrics/cli/batch.py +++ b/detectionmetrics/cli/batch.py @@ -1,4 +1,4 @@ -from itertools import product +from itertools import product, chain from glob import glob import os @@ -30,9 +30,19 @@ def batch(command, jobs_cfg): for model_cfg in jobs_cfg["model"]: model_path = model_cfg["path"] - model_paths = glob(model_path) if model_cfg["path_is_pattern"] else [model_path] - assert model_paths, f"No files found for pattern {model_cfg['path']}" + is_pattern = model_cfg.get("path_is_pattern", False) + if isinstance(model_path, list): + if is_pattern: + model_paths = list(chain.from_iterable(glob(p) for p in model_path)) + else: + model_paths = model_path + else: + model_paths = glob(model_path) if is_pattern else [model_path] + + if not model_paths: + raise FileNotFoundError(f"No files found for path/pattern: {model_path}") + print(f"Found {len(model_paths)} model(s) for pattern: {model_path}") for new_path in model_paths: assert os.path.exists(new_path), f"File or directory {new_path} not found" @@ -41,7 +51,8 @@ def batch(command, jobs_cfg): if os.path.isfile(new_path): new_model_id, _ = os.path.splitext(new_model_id) - new_model_cfg = model_cfg | { + new_model_cfg = { + **model_cfg, "path": new_path, "id": f"{model_cfg['id']}-{new_model_id.replace('-', '_')}", } @@ -102,9 +113,20 @@ def batch(command, jobs_cfg): "model": model_cfg["path"], "model_ontology": model_cfg["ontology"], "model_cfg": model_cfg["cfg"], - "image_size": model_cfg.get("image_size", None), } ) + + if command == "computational_cost": + if jobs_cfg["input_type"] == "image": + params["image_size"] = model_cfg.get("image_size", [512, 512]) + elif jobs_cfg["input_type"] == "lidar": + params["point_cloud_range"] = model_cfg.get( + "point_cloud_range", [-50, -50, -5, 50, 50, 5] + ) + params["num_points"] = model_cfg.get("num_points", 100000) + else: + raise ValueError(f"Unknown input type: {jobs_cfg['input_type']}") + if has_dataset: dataset_cfg = job_components[1] params.update( diff --git a/detectionmetrics/cli/computational_cost.py b/detectionmetrics/cli/computational_cost.py index 284c6285..402b24c7 100644 --- a/detectionmetrics/cli/computational_cost.py +++ b/detectionmetrics/cli/computational_cost.py @@ -1,7 +1,6 @@ import click from detectionmetrics import cli -from detectionmetrics.utils.io import read_json @click.command(name="computational_cost", help="Estimate model computational cost") @@ -12,8 +11,7 @@ # model @click.option( "--model_format", - type=click.Choice(["torch", "tensorflow"], case_sensitive=False - ), + type=click.Choice(["torch", "tensorflow"], case_sensitive=False), show_default=True, default="torch", help="Trained model format", @@ -38,14 +36,35 @@ ) @click.option( "--image_size", - type=(int, int), + nargs=2, + type=int, required=False, - help="Dummy image size used for computational cost estimation", + help="Dummy image size. Should be provided as two integers: width height", +) +@click.option( + "--point_cloud_range", + nargs=6, + type=int, + required=False, + help="Dummy point cloud range (meters). Should be provided as six integers: x_min y_min z_min x_max y_max z_max", +) +@click.option( + "--num_points", + type=int, + required=False, + help="Number of points for the dummy point cloud (uniformly sampled)", +) +@click.option( + "--has_intensity", + is_flag=True, + default=False, + help="Whether the dummy point cloud has intensity values", ) # output @click.option( "--out_fname", type=click.Path(writable=True), + required=True, help="CSV file where the computational cost estimation results will be stored", ) def computational_cost( @@ -56,23 +75,46 @@ def computational_cost( model_ontology, model_cfg, image_size, + point_cloud_range, + num_points, + has_intensity, out_fname, ): """Estimate model computational cost""" - - if image_size is None: - parsed_model_cfg = read_json(model_cfg) - if "image_size" in parsed_model_cfg: - image_size = parsed_model_cfg["image_size"] - else: + if input_type == "image": + if image_size is None: + raise ValueError("Image size must be provided for image models") + if point_cloud_range is not None or num_points is not None: + raise ValueError( + "Point cloud range and number of points cannot be provided for image models" + ) + if has_intensity: + raise ValueError("Intensity flag cannot be set for image models") + params = {"image_size": image_size} + elif input_type == "lidar": + if point_cloud_range is None or num_points is None: raise ValueError( - "Image size must be provided either as an argument or in the model configuration file" + "Point cloud range and number of points must be provided for lidar models" ) + if image_size is not None: + raise ValueError("Image size cannot be provided for lidar models") + + params = { + "point_cloud_range": point_cloud_range, + "num_points": num_points, + "has_intensity": has_intensity, + } + else: + raise ValueError(f"Unknown input type: {input_type}") model = cli.get_model( task, input_type, model_format, model, model_ontology, model_cfg ) - results = model.get_computational_cost(image_size) + results = model.get_computational_cost(**params) results.to_csv(out_fname) return results + + +if __name__ == "__main__": + computational_cost() diff --git a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py index 14a85b16..2bdb96ac 100644 --- a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py +++ b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py @@ -1,3 +1,4 @@ +import time from typing import List, Optional, Tuple from c_gen_normal_map import gen_normal_map @@ -144,7 +145,8 @@ def get_sample( name: Optional[str] = None, idx: Optional[int] = None, has_intensity: bool = True, -) -> dict: + measure_processing_time: bool = False +) -> Tuple[dict, Optional[dict]]: """Get sample data for mmdetection3d models :param points_fname: filename of the point cloud @@ -159,8 +161,10 @@ def get_sample( :type idx: Optional[int], optional :param has_intensity: whether the point cloud has intensity values, defaults to True :type has_intensity: bool, optional - :return: Sample data dictionary - :rtype: dict + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: sample data dictionary and processing time dictionary (if measured) + :rtype: Tuple[dict, Optional[dict]] """ raw_data = ul.read_semantickitti_points(points_fname, has_intensity) @@ -170,6 +174,9 @@ def get_sample( labels = labels.reshape((-1, 1)).astype(np.uint8) ref_labels = labels.copy() + if measure_processing_time: + start = time.perf_counter() + xyz = raw_data[:, :3] feat = raw_data[:, 3:4] if model_cfg["n_feats"] > 3 else None origin_len = len(xyz) @@ -206,6 +213,10 @@ def get_sample( unproj_normal_data = compute_normals_range(feat) + if measure_processing_time: + end = time.perf_counter() + processing_time = {"preprocessing": end - start} + sample = {} sample["point_feat"] = feat sample["point_label"] = labels @@ -219,11 +230,14 @@ def get_sample( sample["sample_id"] = name sample["idx"] = idx + if measure_processing_time: + return sample, processing_time + return sample def inference( - sample: dict, model: torch.nn.Module, model_cfg: dict + sample: dict, model: torch.nn.Module, model_cfg: dict, measure_processing_time: bool = False ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: """Perform inference on a sample using an mmdetection3D model @@ -233,8 +247,10 @@ def inference( :type model: torch.nn.Module :param model_cfg: model configuration :type model_cfg: dict - :return: predictions, labels, and sample names - :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: tuple of (predictions, labels, names) and processing time dictionary (if measured) + :rtype: Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]] """ single_sample = not isinstance(sample["sample_id"], list) if single_sample: @@ -244,7 +260,15 @@ def inference( for k, v in sample.items(): sample[k] = ut.data_to_device(v, device) + if measure_processing_time: + torch.cuda.synchronize() + start = time.perf_counter() pred = model(sample) + if measure_processing_time: + torch.cuda.synchronize() + end = time.perf_counter() + processing_time = {"inference_n_voxelization": end - start} + pred["logits"] = torch.argmax(pred["logits"], dim=1) has_labels = pred["labels"][0] is not None @@ -260,4 +284,7 @@ def inference( if has_labels: labels = torch.stack(labels, dim=0).squeeze() + if measure_processing_time: + return (preds, labels, names), processing_time + return preds, labels, names diff --git a/detectionmetrics/models/lidar_torch_utils/mmdet3d.py b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py index a003089a..c377e87d 100644 --- a/detectionmetrics/models/lidar_torch_utils/mmdet3d.py +++ b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py @@ -1,3 +1,4 @@ +import time from typing import List, Optional, Tuple from mmdet3d.datasets.transforms import ( @@ -19,7 +20,8 @@ def get_sample( name: Optional[str] = None, idx: Optional[int] = None, has_intensity: bool = True, -) -> dict: + measure_processing_time: bool = False, +) -> Tuple[dict, Optional[dict]]: """Get sample data for mmdetection3d models :param points_fname: filename of the point cloud @@ -34,8 +36,10 @@ def get_sample( :type idx: Optional[int], optional :param has_intensity: whether the point cloud has intensity values, defaults to True :type has_intensity: bool, optional - :return: Sample data dictionary - :rtype: dict + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: sample data and optionally processing time + :rtype: Tuple[ dict, Optional[dict] ] """ sample = { "lidar_points": { @@ -71,14 +75,26 @@ def get_sample( meta_keys=["sample_idx", "lidar_path", "num_pts_feats", "sample_id"], ) ) + + if measure_processing_time: + start = time.perf_counter() transforms = Compose(transforms) + sample = transforms(sample) + if measure_processing_time: + end = time.perf_counter() + return sample, {"preprocessing": end - start} - return transforms(sample) + return sample def inference( - sample: dict, model: torch.nn.Module, model_cfg: dict -) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: + sample: dict, + model: torch.nn.Module, + model_cfg: dict, + measure_processing_time: bool = False, +) -> Tuple[ + Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]], Optional[dict] +]: """Perform inference on a sample using an mmdetection3D model :param sample: sample data dictionary @@ -87,18 +103,33 @@ def inference( :type model: torch.nn.Module :param model_cfg: model configuration :type model_cfg: dict - :return: predictions, labels, and sample names - :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: predictions, labels (if available), sample names and optionally processing time + :rtype: Tuple[ Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str """ single_sample = not isinstance(sample["data_samples"], list) if single_sample: sample = COLLATE_FN([sample]) + if measure_processing_time: + start = time.perf_counter() sample = model.data_preprocessor(sample, training=False) + if measure_processing_time: + end = time.perf_counter() + processing_time = {"voxelization": end - start} + inputs, data_samples = sample["inputs"], sample["data_samples"] has_labels = hasattr(data_samples[0].gt_pts_seg, "pts_semantic_mask") + if measure_processing_time: + torch.cuda.synchronize() + start = time.perf_counter() outputs = model(inputs, data_samples, mode="predict") + if measure_processing_time: + torch.cuda.synchronize() + end = time.perf_counter() + processing_time["inference"] = end - start preds, labels, names = ([], [], []) if has_labels else ([], None, None) for output in outputs: @@ -110,4 +141,7 @@ def inference( if has_labels: labels = torch.stack(labels, dim=0).squeeze() - return preds, labels, names + if measure_processing_time: + return (preds, labels, names), processing_time + else: + return preds, labels, names diff --git a/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py index 61060c0f..7dab1b0d 100644 --- a/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py +++ b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py @@ -1,4 +1,5 @@ -from typing import Optional, Tuple +import time +from typing import Optional, Tuple, Union, Dict import numpy as np import torch @@ -18,7 +19,11 @@ def inference( sample: Tuple[np.ndarray, np.ndarray, ul.Sampler], model: torch.nn.Module, model_cfg: dict, -) -> torch.Tensor: + measure_processing_time: bool = False, +) -> Union[ + Tuple[torch.Tensor, Optional[torch.Tensor], Optional[str]], + Tuple[torch.Tensor, Optional[torch.Tensor], Optional[str], Dict[str, float]], +]: """Perform inference on a sample using an Open3D-ML model :param sample: sample data dictionary @@ -27,14 +32,18 @@ def inference( :type model: torch.nn.Module :param model_cfg: model configuration :type model_cfg: dict - :return: predictions, labels, and sample names - :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: predicted labels, ground truth labels, sample name and optionally processing time + :rtype: Union[ Tuple[torch.Tensor, Optional[torch.Tensor], Optional[str]], Tuple[torch.Tensor, Optional[torch.Tensor], Optional[str], Dict[str, float]] ] """ infer_complete = False points, projected_indices, sampler, label, name, _ = sample model_format = model_cfg["model_format"] end_th = model_cfg.get("end_th", 0.5) + processing_time = {"preprocessing": 0, "inference": 0} + if "kpconv" in model_format: transform_input = kpconv.transform_input update_probs = kpconv.update_probs @@ -48,20 +57,35 @@ def inference( while not infer_complete: # Get model input data + if measure_processing_time: + start = time.perf_counter() input_data, selected_indices = transform_input(points, model_cfg, sampler) + if measure_processing_time: + end = time.perf_counter() + processing_time["preprocessing"] += end - start + input_data = ut.data_to_device(input_data, model.device) if "randlanet" in model_format: input_data = ut.unsqueeze_data(input_data) # Perform inference with torch.no_grad(): + if measure_processing_time: + torch.cuda.synchronize() + start = time.perf_counter() pred = model(*input_data) + if measure_processing_time: + torch.cuda.synchronize() + end = time.perf_counter() + processing_time["inference"] += end - start # TODO: check if this is consistent across different models if isinstance(pred, dict): pred = pred["out"] # Update probabilities if sampler is used + if measure_processing_time: + start = time.perf_counter() if sampler is not None: if "kpconv" in model_format: sampler.test_probs = update_probs( @@ -83,11 +107,20 @@ def inference( else: pred = pred.squeeze().cpu()[projected_indices].cuda() infer_complete = True + if measure_processing_time: + end = time.perf_counter() + processing_time["postprocessing"] += end - start if label is not None: label = torch.from_numpy(label.astype(np.int64)).long().cuda() - return torch.argmax(pred.squeeze(), axis=-1), label, name + result = torch.argmax(pred.squeeze(), axis=-1), label, name + + # Return processing time if needed + if measure_processing_time: + return result, processing_time + + return result def get_sample( @@ -97,7 +130,14 @@ def get_sample( name: Optional[str] = None, idx: Optional[int] = None, has_intensity: bool = True, -) -> Tuple[np.ndarray, np.ndarray, ul.Sampler]: + measure_processing_time: bool = False, +) -> Tuple[ + Union[ + Tuple[np.ndarray, np.ndarray, ul.Sampler, np.ndarray, str, int], + Tuple[np.ndarray, np.ndarray, ul.Sampler, np.ndarray, str, int], + Dict[str, float], + ] +]: """Get sample data for mmdetection3d models :param points_fname: filename of the point cloud @@ -112,14 +152,19 @@ def get_sample( :type idx: Optional[int], optional :param has_intensity: whether the point cloud has intensity values, defaults to True :type has_intensity: bool, optional - :return: Sample data tuple - :rtype: Tuple[np.ndarray, np.ndarray, ul.Sampler, np.ndarray, str, int] + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: sample data and optionally processing time + :rtype: Union[ Tuple[np.ndarray, np.ndarray, ul.Sampler, np.ndarray, str, int], Tuple[np.ndarray, np.ndarray, ul.Sampler, np.ndarray, str, int], Dict[str, float] ] """ points = ul.read_semantickitti_points(points_fname, has_intensity) label = None if label_fname is not None: label, _ = ul.read_semantickitti_label(label_fname) + if measure_processing_time: + start = time.perf_counter() + # Keep only XYZ coordinates points = np.array(points[:, 0:3], dtype=np.float32) @@ -142,4 +187,30 @@ def get_sample( model_cfg["n_classes"], ) - return sub_points, projected_indices, sampler, label, name, idx + if measure_processing_time: + end = time.perf_counter() + + sample = sub_points, projected_indices, sampler, label, name, idx + + # Return processing time if needed + if measure_processing_time: + processing_time = {"preprocessing": end - start} + return sample, processing_time + + return sample + + +def reset_sampler(sampler: ul.Sampler, num_points: int, num_classes: int): + """Reset sampler object probabilities + + :param sampler: Sampler object + :type sampler: ul.Sampler + :param num_points: Number of points in the point cloud + :type num_points: int + :param num_classes: Number of semantic classes + :type num_classes: int + """ + sampler.p = np.random.rand(num_points) * 1e-3 + sampler.min_p = float(np.min(sampler.p[-1])) + sampler.test_probs = np.zeros((num_points, num_classes), dtype=np.float32) + return sampler diff --git a/detectionmetrics/models/lidar_torch_utils/sphereformer.py b/detectionmetrics/models/lidar_torch_utils/sphereformer.py index 92f0554e..0d90cee7 100644 --- a/detectionmetrics/models/lidar_torch_utils/sphereformer.py +++ b/detectionmetrics/models/lidar_torch_utils/sphereformer.py @@ -1,3 +1,4 @@ +import time from typing import List, Optional, Tuple import numpy as np @@ -54,7 +55,8 @@ def get_sample( name: Optional[str] = None, idx: Optional[int] = None, has_intensity: bool = True, -) -> dict: + measure_processing_time: bool = False +) -> Tuple[dict, Optional[dict]]: """Get sample data for mmdetection3d models :param points_fname: filename of the point cloud @@ -69,8 +71,10 @@ def get_sample( :type idx: Optional[int], optional :param has_intensity: whether the point cloud has intensity values, defaults to True :type has_intensity: bool, optional - :return: Sample data dictionary - :rtype: dict + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: sample data dictionary and processing time dictionary (if measured) + :rtype: Tuple[dict, Optional[dict]] """ feats = ul.read_semantickitti_points(points_fname, has_intensity) feats = feats[:, : model_cfg["n_feats"]] @@ -81,11 +85,12 @@ def get_sample( annotated_data = annotated_data.reshape((-1, 1)) labels_in = annotated_data.astype(np.uint8).reshape(-1) - xyz = feats[:, :3] + if measure_processing_time: + start = time.perf_counter() + xyz = feats[:, :3] xyz = np.clip(xyz, model_cfg["pc_range"][0], model_cfg["pc_range"][1]) - coords, xyz, feats, labels, inds_reconstruct = data_prepare( xyz, feats, @@ -97,6 +102,10 @@ def get_sample( model_cfg["xyz_norm"], ) + if measure_processing_time: + end = time.perf_counter() + processing_time = {"voxelization": end - start} + sample = ( coords, xyz, @@ -107,12 +116,15 @@ def get_sample( name, ) + if measure_processing_time: + return sample, processing_time + return sample def inference( - sample: dict, model: torch.nn.Module, model_cfg: dict -) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: + sample: dict, model: torch.nn.Module, model_cfg: dict, measure_processing_time: bool = False +) -> Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]]: """Perform inference on a sample using an mmdetection3D model :param sample: sample data dictionary @@ -121,8 +133,10 @@ def inference( :type model: torch.nn.Module :param model_cfg: model configuration :type model_cfg: dict - :return: predictions, labels, and sample names - :rtype: Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]] + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: tuple of (predictions, labels, names) and processing time dictionary (if measured) + :rtype: Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]] """ single_sample = not isinstance(sample[-1], list) if single_sample: @@ -142,6 +156,9 @@ def inference( names, ) = sample + if measure_processing_time: + start = time.perf_counter() + offset_ = offset.clone() offset_[1:] = offset_[1:] - offset_[:-1] @@ -156,9 +173,24 @@ def inference( batch_size = len(fnames) sinput = spconv.SparseConvTensor(feat, coord.int(), spatial_shape, batch_size) - + if measure_processing_time: + end = time.perf_counter() + processing_time = {"preprocessing": end - start} + start = time.perf_counter() + + if measure_processing_time: + torch.cuda.synchronize() + start = time.perf_counter() preds = model(sinput, xyz, batch) + if measure_processing_time: + torch.cuda.synchronize() + end = time.perf_counter() + processing_time["inference"] = end - start + preds = preds[inds_reconstruct, :] preds = torch.argmax(preds, dim=1) + if measure_processing_time: + return (preds, labels, names), processing_time + return preds, labels, names diff --git a/detectionmetrics/models/tensorflow.py b/detectionmetrics/models/tensorflow.py index 6207879f..ee6035dc 100644 --- a/detectionmetrics/models/tensorflow.py +++ b/detectionmetrics/models/tensorflow.py @@ -18,71 +18,6 @@ tf.config.optimizer.set_experimental_options({"layout_optimizer": False}) -def get_computational_cost( - model: tf.Module, - dummy_input: tf.Tensor, - model_fname: Optional[str] = None, - runs: int = 30, - warm_up_runs: int = 5, -) -> dict: - """Get different metrics related to the computational cost of the model - - :param model: Loaded TensorFlow SavedModel - :type model: tf.Module - :param dummy_input: Dummy input data for the model - :type dummy_input: tf.Tensor - :param model_fname: Model filename used to measure model size, defaults to None - :type model_fname: Optional[str], optional - :param runs: Number of runs to measure inference time, defaults to 30 - :type runs: int, optional - :param warm_up_runs: Number of warm-up runs, defaults to 5 - :type warm_up_runs: int, optional - :return: DataFrame containing computational cost information - :rtype: pd.DataFrame - """ - # Get model size (if possible) and number of parameters - if model_fname is not None: - size_mb = sum( - os.path.getsize(os.path.join(dirpath, f)) - for dirpath, _, files in os.walk(model_fname) - for f in files - ) - size_mb /= 1024**2 - else: - size_mb = None - - n_params = sum(np.prod(var.shape) for var in model.variables.variables) - - # Measure inference time with GPU synchronization - infer = model.signatures["serving_default"] - for _ in range(warm_up_runs): - _ = infer(dummy_input) - - has_gpu = bool(tf.config.list_physical_devices("GPU")) - inference_times = [] - - for _ in range(runs): - if has_gpu: - tf.config.experimental.set_synchronous_execution(True) - - start_time = time.time() - _ = infer(dummy_input) - - if has_gpu: - tf.config.experimental.set_synchronous_execution(True) - - inference_times.append(time.time() - start_time) - - # Retrieve computational cost information - result = { - "input_shape": ["x".join(map(str, dummy_input.shape.as_list()))], - "n_params": [int(n_params)], - "size_mb": [size_mb], - "inference_time_s": [np.mean(inference_times)], - } - return pd.DataFrame.from_dict(result) - - def resize_image( image: tf.Tensor, method: str, @@ -510,7 +445,46 @@ def get_computational_cost( :type warm_up_runs: int, optional :return: Dictionary containing computational cost information """ + # Generate dummy input dummy_input = tf.random.normal([1, *image_size, 3]) - return get_computational_cost( - self.model, dummy_input, self.model_fname, runs, warm_up_runs - ) + + # Get model size (if possible) and number of parameters + if self.model_fname is not None: + size_mb = sum( + os.path.getsize(os.path.join(dirpath, f)) + for dirpath, _, files in os.walk(self.model_fname) + for f in files + ) + size_mb /= 1024**2 + else: + size_mb = None + + n_params = sum(np.prod(var.shape) for var in self.model.variables.variables) + + # Measure inference time with GPU synchronization + for _ in range(warm_up_runs): + self.predict(dummy_input) + + has_gpu = bool(tf.config.list_physical_devices("GPU")) + inference_times = [] + + for _ in range(runs): + if has_gpu: + tf.config.experimental.set_synchronous_execution(True) + + start_time = time.time() + self.predict(dummy_input) + + if has_gpu: + tf.config.experimental.set_synchronous_execution(True) + + inference_times.append(time.time() - start_time) + + # Retrieve computational cost information + result = { + "input_shape": ["x".join(map(str, dummy_input.shape.as_list()))], + "n_params": [int(n_params)], + "size_mb": [size_mb], + "inference_time_s": [np.mean(inference_times)], + } + return pd.DataFrame.from_dict(result) diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index 768997e8..c4328c14 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -1,6 +1,7 @@ import importlib import os import time +import tempfile from typing import Any, List, Optional, Tuple, Union import numpy as np @@ -59,63 +60,6 @@ def get_computational_cost( :return: DataFrame containing computational cost information :rtype: pd.DataFrame """ - # Get model size if possible - if model_fname is not None: - size_mb = os.path.getsize(model_fname) / 1024**2 - else: - size_mb = None - - # Measure inference time with GPU synchronization - dummy_tuple = dummy_input if isinstance(dummy_input, tuple) else (dummy_input,) - - for _ in range(warm_up_runs): - if hasattr(model, "inference"): # e.g. mmsegmentation models - model.inference( - *dummy_tuple, - [ - dict( - ori_shape=dummy_tuple[0].shape[2:], - img_shape=dummy_tuple[0].shape[2:], - pad_shape=dummy_tuple[0].shape[2:], - padding_size=[0, 0, 0, 0], - ) - ] - * dummy_tuple[0].shape[0], - ) - else: - model(*dummy_tuple) - - inference_times = [] - for _ in range(runs): - torch.cuda.synchronize() - start_time = time.time() - if hasattr(model, "inference"): # e.g. mmsegmentation models - model.inference( - *dummy_tuple, - [ - dict( - ori_shape=dummy_tuple[0].shape[2:], - img_shape=dummy_tuple[0].shape[2:], - pad_shape=dummy_tuple[0].shape[2:], - padding_size=[0, 0, 0, 0], - ) - ] - * dummy_tuple[0].shape[0], - ) - else: - model(*dummy_tuple) - torch.cuda.synchronize() - end_time = time.time() - inference_times.append(end_time - start_time) - - result = { - "input_shape": ["x".join(map(str, ut.get_data_shape(dummy_input)))], - "n_params": [sum(p.numel() for p in model.parameters())], - "size_mb": [size_mb], - "inference_time_s": [np.mean(inference_times)], - } - - return pd.DataFrame.from_dict(result) class CustomResize(torch.nn.Module): @@ -544,10 +488,38 @@ def get_computational_cost( :type warm_up_runs: int, optional :return: Dictionary containing computational cost information """ + # Create dummy input dummy_input = torch.randn(1, 3, *image_size).to(self.device) - return get_computational_cost( - self.model, dummy_input, self.model_fname, runs, warm_up_runs - ) + + # Get model size if possible + if self.model_fname is not None: + size_mb = os.path.getsize(self.model_fname) / 1024**2 + else: + size_mb = None + + # Measure inference time with GPU synchronization + dummy_tuple = dummy_input if isinstance(dummy_input, tuple) else (dummy_input,) + + for _ in range(warm_up_runs): + self.predict(dummy_tuple[0]) + + inference_times = [] + for _ in range(runs): + torch.cuda.synchronize() + start_time = time.time() + self.predict(dummy_tuple[0]) + torch.cuda.synchronize() + end_time = time.time() + inference_times.append(end_time - start_time) + + result = { + "input_shape": ["x".join(map(str, ut.get_data_shape(dummy_input)))], + "n_params": [sum(p.numel() for p in self.model.parameters())], + "size_mb": [size_mb], + "inference_time_s": [np.mean(inference_times)], + } + + return pd.DataFrame.from_dict(result) class TorchLiDARSegmentationModel(dm_model.LiDARSegmentationModel): @@ -607,7 +579,11 @@ def __init__( except ImportError: raise_unknown_model_format_lidar(model_format) self._get_sample = model_utils_module.get_sample - self._inference = model_utils_module.inference + self.inference = model_utils_module.inference + if hasattr(model_utils_module, "reset_sampler"): + self._reset_sampler = model_utils_module.reset_sampler + else: + self._reset_sampler = None def predict( self, @@ -736,25 +712,80 @@ def eval( return um.get_metrics_dataframe(metrics_factory, self.ontology) - def get_computational_cost(self, runs: int = 30, warm_up_runs: int = 5) -> dict: + def get_computational_cost( + self, + point_cloud_range: Tuple[int, int, int, int, int, int] = ( + -50, + -50, + -5, + 50, + 50, + 5, + ), + num_points: int = 100000, + has_intensity: bool = False, + runs: int = 30, + warm_up_runs: int = 5, + ) -> dict: """Get different metrics related to the computational cost of the model + :param point_cloud_range: Point cloud range (meters), defaults to (-50, -50, -5, 50, 50, 5) + :type point_cloud_range: Tuple[int, int, int, int, int, int], optional + :param num_points: Number of points in the point cloud, defaults to 100000 + :type num_points: int, optional + :param has_intensity: Whether the point cloud has intensity values, defaults to False + :type has_intensity: bool, optional :param runs: Number of runs to measure inference time, defaults to 30 :type runs: int, optional :param warm_up_runs: Number of warm-up runs, defaults to 5 :type warm_up_runs: int, optional :return: Dictionary containing computational cost information """ - # Build dummy input data (process is a bit complex for LiDAR models) - dummy_points = np.random.rand(1000000, 4) - dummy_points, _, sampler = self.preprocess(dummy_points, self.model_cfg) - - dummy_input, _ = self.transform_input(dummy_points, self.model_cfg, sampler) - dummy_input = ut.data_to_device(dummy_input, self.device) - if self.model_format != "o3d_kpconv": - dummy_input = ut.unsqueeze_data(dummy_input) - - # Get computational cost - return get_computational_cost( - self.model, dummy_input, self.model_fname, runs, warm_up_runs - ) + # Build dummy point cloud using uniform distribution + dummy_points = np.random.uniform( + low=point_cloud_range[0:3], + high=point_cloud_range[3:6], + size=(num_points, 3 + int(has_intensity)), + ).astype(np.float32) + + # Store in a secure temporary .bin file + with tempfile.NamedTemporaryFile(suffix=".bin") as tmp_file: + dummy_points.tofile(tmp_file.name) + sample = self._get_sample( + tmp_file.name, self.model_cfg, has_intensity=has_intensity + ) + + # Get model size if possible + if self.model_fname is not None: + size_mb = os.path.getsize(self.model_fname) / 1024**2 + else: + size_mb = None + + # Measure inference time with GPU synchronization + for _ in range(warm_up_runs): + if "o3d" in self.model_format: # reset random sampling for Open3D-ML models + subsampled_points, _, sampler, _, _, _ = sample + self._reset_sampler(sampler, subsampled_points.shape[0], self.n_classes) + + self.inference(sample, self.model, self.model_cfg) + + inference_times = [] + for _ in range(runs): + if "o3d" in self.model_format: # reset random sampling for Open3D-ML models + subsampled_points, _, sampler, _, _, _ = sample + self._reset_sampler(sampler, subsampled_points.shape[0], self.n_classes) + torch.cuda.synchronize() + start_time = time.time() + self.inference(sample, self.model, self.model_cfg) + torch.cuda.synchronize() + end_time = time.time() + inference_times.append(end_time - start_time) + + result = { + "input_shape": ["x".join(map(str, ut.get_data_shape(dummy_points)))], + "n_params": [sum(p.numel() for p in self.model.parameters())], + "size_mb": [size_mb], + "inference_time_s": [np.mean(inference_times)], + } + + return pd.DataFrame.from_dict(result) From 4b33ce892e3c86ea3cf992056b13206b5ec121c9 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Fri, 3 Oct 2025 12:50:22 +0200 Subject: [PATCH 28/39] Allow for optionally returning sample along with model prediction --- detectionmetrics/models/lidar_torch_utils/o3d/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py index 7dab1b0d..8d69027b 100644 --- a/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py +++ b/detectionmetrics/models/lidar_torch_utils/o3d/__init__.py @@ -42,7 +42,7 @@ def inference( model_format = model_cfg["model_format"] end_th = model_cfg.get("end_th", 0.5) - processing_time = {"preprocessing": 0, "inference": 0} + processing_time = {"preprocessing": 0, "inference": 0, "postprocessing": 0} if "kpconv" in model_format: transform_input = kpconv.transform_input From ae7c078442b5b95a7586b67a289dd49ab6c2d703 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Fri, 3 Oct 2025 12:53:20 +0200 Subject: [PATCH 29/39] Allow for optionally returning sample along with model prediction --- detectionmetrics/models/model.py | 35 ++++++++++++++------ detectionmetrics/models/tensorflow.py | 29 +++++++++++------ detectionmetrics/models/torch.py | 46 ++++++++++++++++++--------- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/detectionmetrics/models/model.py b/detectionmetrics/models/model.py index 4439e3b9..7d56b539 100644 --- a/detectionmetrics/models/model.py +++ b/detectionmetrics/models/model.py @@ -52,7 +52,9 @@ def __init__( self.model_cfg["n_classes"] = self.n_classes @abstractmethod - def predict(self, data: Union[np.ndarray, Image.Image]) -> Union[np.ndarray, Image.Image]: + def predict( + self, data: Union[np.ndarray, Image.Image] + ) -> Union[np.ndarray, Image.Image]: """Perform prediction for a single data sample :param data: Input data sample (image or point cloud) @@ -148,13 +150,17 @@ def __init__( super().__init__(model, model_type, model_cfg, ontology_fname, model_fname) @abstractmethod - def predict(self, image: Image.Image) -> Image.Image: - """Perform inference for a single image + def predict( + self, image: Image.Image, return_sample: bool = False + ) -> Union[Image.Image, Tuple[Image.Image, Any]]: + """Perform prediction for a single image - :param image: PIL image. + :param image: PIL image :type image: Image.Image - :return: Segmenation result as PIL image - :rtype: Image.Image + :param return_sample: Whether to return the sample data along with predictions, defaults to False + :type return_sample: bool, optional + :return: Segmentation result as a PIL image or a tuple with the segmentation result and the input sample tensor + :rtype: Union[Image.Image, Tuple[Image.Image, Any]] """ raise NotImplementedError @@ -230,13 +236,22 @@ def __init__( super().__init__(model, model_type, model_cfg, ontology_fname, model_fname) @abstractmethod - def predict(self, points_fname: np.ndarray) -> np.ndarray: - """Perform inference for a single point cloud + def predict( + self, + points_fname: str, + has_intensity: bool = True, + return_sample: bool = False, + ) -> Union[np.ndarray, Tuple[np.ndarray, Any]]: + """Perform prediction for a single point cloud :param points_fname: Point cloud in SemanticKITTI .bin format :type points_fname: str - :return: Segmenation result as a point cloud with label indices - :rtype: np.ndarray + :param has_intensity: Whether the point cloud has intensity values, defaults to True + :type has_intensity: bool, optional + :param return_sample: Whether to return the sample data along with predictions, defaults to False + :type return_sample: bool, optional + :return: Segmentation result as a numpy array or a tuple with the segmentation result and the input sample data + :rtype: Union[np.ndarray, Tuple[np.ndarray, Any]] """ raise NotImplementedError diff --git a/detectionmetrics/models/tensorflow.py b/detectionmetrics/models/tensorflow.py index ee6035dc..55966f76 100644 --- a/detectionmetrics/models/tensorflow.py +++ b/detectionmetrics/models/tensorflow.py @@ -296,19 +296,28 @@ def t_in(image): tf.argmax(tf.squeeze(x), axis=2).numpy().astype(np.uint8) ) - def predict(self, image: Image.Image) -> Image.Image: + def predict( + self, image: Image.Image, return_sample: bool = False + ) -> Union[Image.Image, Tuple[Image.Image, tf.Tensor]]: """Perform prediction for a single image :param image: PIL image :type image: Image.Image - :return: Segmentation result as PIL image - :rtype: Image.Image + :param return_sample: Whether to return the sample data along with predictions, defaults to False + :type return_sample: bool, optional + :return: Segmentation result as a PIL image or a tuple with the segmentation result and the input sample tensor + :rtype: Union[Image.Image, Tuple[Image.Image, tf.Tensor]] """ - tensor = self.t_in(image) - result = self.predict(tensor) - return self.t_out(result) + sample = self.t_in(image) + result = self.inference(sample) + result = self.t_out(result) - def predict(self, tensor_in: tf.Tensor) -> tf.Tensor: + if return_sample: + return result, sample + else: + return result + + def inference(self, tensor_in: tf.Tensor) -> tf.Tensor: """Perform inference for a tensor :param tensor_in: Input point cloud tensor @@ -390,7 +399,7 @@ def eval( for idx, image, label in pbar: idx = idx.numpy() - pred = self.predict(image) + pred = self.inference(image) # Get valid points masks depending on ignored label indices if ignored_label_indices: @@ -463,7 +472,7 @@ def get_computational_cost( # Measure inference time with GPU synchronization for _ in range(warm_up_runs): - self.predict(dummy_input) + self.inference(dummy_input) has_gpu = bool(tf.config.list_physical_devices("GPU")) inference_times = [] @@ -473,7 +482,7 @@ def get_computational_cost( tf.config.experimental.set_synchronous_execution(True) start_time = time.time() - self.predict(dummy_input) + self.inference(dummy_input) if has_gpu: tf.config.experimental.set_synchronous_execution(True) diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index c4328c14..cca3f1f5 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -318,19 +318,28 @@ def __init__( ] ) - def predict(self, image: Image.Image) -> Image.Image: + def predict( + self, image: Image.Image, return_sample: bool = False + ) -> Union[Image.Image, Tuple[Image.Image, torch.Tensor]]: """Perform prediction for a single image :param image: PIL image :type image: Image.Image - :return: Segmentation result as a PIL image - :rtype: Image.Image + :param return_sample: Whether to return the sample data along with predictions, defaults to False + :type return_sample: bool, optional + :return: Segmentation result as a PIL image or a tuple with the segmentation result and the input sample tensor + :rtype: Union[Image.Image, Tuple[Image.Image, torch.Tensor]] """ - tensor = self.transform_input(image).unsqueeze(0).to(self.device) - result = self.predict(tensor) - return self.transform_output(result) + sample = self.transform_input(image).unsqueeze(0).to(self.device) + result = self.inference(sample) + result = self.transform_output(result) + + if return_sample: + return result, sample + else: + return result - def predict(self, tensor_in: torch.Tensor) -> torch.Tensor: + def inference(self, tensor_in: torch.Tensor) -> torch.Tensor: """Perform inference for a tensor :param tensor_in: Input point cloud tensor @@ -425,7 +434,7 @@ def eval( pbar = tqdm(dataloader, leave=True) for idx, image, label in pbar: # Perform inference - pred = self.predict(image) + pred = self.inference(image) # Get valid points masks depending on ignored label indices if ignored_label_indices: @@ -501,13 +510,13 @@ def get_computational_cost( dummy_tuple = dummy_input if isinstance(dummy_input, tuple) else (dummy_input,) for _ in range(warm_up_runs): - self.predict(dummy_tuple[0]) + self.inference(dummy_tuple[0]) inference_times = [] for _ in range(runs): torch.cuda.synchronize() start_time = time.time() - self.predict(dummy_tuple[0]) + self.inference(dummy_tuple[0]) torch.cuda.synchronize() end_time = time.time() inference_times.append(end_time - start_time) @@ -589,23 +598,30 @@ def predict( self, points_fname: str, has_intensity: bool = True, - ) -> np.ndarray: + return_sample: bool = False, + ) -> Union[np.ndarray, Tuple[np.ndarray, Any]]: """Perform prediction for a single point cloud :param points_fname: Point cloud in SemanticKITTI .bin format :type points_fname: str :param has_intensity: Whether the point cloud has intensity values, defaults to True :type has_intensity: bool, optional - :return: Segmenation result as a point cloud with label indices - :rtype: np.ndarray + :param return_sample: Whether to return the sample data along with predictions, defaults to False + :type return_sample: bool, optional + :return: Segmentation result as a numpy array or a tuple with the segmentation result and the input sample data + :rtype: Union[np.ndarray, Tuple[np.ndarray, Any]] """ # Preprocess point cloud sample = self._get_sample( points_fname, self.model_cfg, has_intensity=has_intensity ) - pred, _, _ = self.inference(sample, self.model, self.model_cfg) + result, _, _ = self.inference(sample, self.model, self.model_cfg) + result = result.squeeze().cpu().numpy() - return pred.squeeze().cpu().numpy() + if return_sample: + return result, sample + else: + return result def eval( self, From af78c4569b0fd1cd707d6bde9e95607f3c9768bf Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Thu, 18 Dec 2025 11:07:57 +0100 Subject: [PATCH 30/39] Remove legacy code --- detectionmetrics/utils/metrics.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/detectionmetrics/utils/metrics.py b/detectionmetrics/utils/metrics.py index 1d563b34..b197dc74 100644 --- a/detectionmetrics/utils/metrics.py +++ b/detectionmetrics/utils/metrics.py @@ -50,10 +50,6 @@ def update( if valid_mask is not None: mask &= valid_mask - # Update confusion matrix - if np.count_nonzero(gt >= 16): - pass - # Update confusion matrix new_entry = np.bincount( self.n_classes * gt[mask].astype(int) + pred[mask].astype(int), From 88de873e0d493eaa3213c9d0003169c90c48790b Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Thu, 18 Dec 2025 11:08:48 +0100 Subject: [PATCH 31/39] Allow for ignoring index during LiDAR inference --- .../models/lidar_torch_utils/lsk3dnet.py | 12 ++++++++++-- detectionmetrics/models/lidar_torch_utils/mmdet3d.py | 10 ++++++++-- .../models/lidar_torch_utils/sphereformer.py | 12 ++++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py index 2bdb96ac..581c2e05 100644 --- a/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py +++ b/detectionmetrics/models/lidar_torch_utils/lsk3dnet.py @@ -145,7 +145,7 @@ def get_sample( name: Optional[str] = None, idx: Optional[int] = None, has_intensity: bool = True, - measure_processing_time: bool = False + measure_processing_time: bool = False, ) -> Tuple[dict, Optional[dict]]: """Get sample data for mmdetection3d models @@ -237,7 +237,11 @@ def get_sample( def inference( - sample: dict, model: torch.nn.Module, model_cfg: dict, measure_processing_time: bool = False + sample: dict, + model: torch.nn.Module, + model_cfg: dict, + ignore_index: Optional[List[int]] = None, + measure_processing_time: bool = False, ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: """Perform inference on a sample using an mmdetection3D model @@ -247,6 +251,8 @@ def inference( :type model: torch.nn.Module :param model_cfg: model configuration :type model_cfg: dict + :param ignore_index: list of class indices to ignore during inference, defaults to None + :type ignore_index: Optional[List[int]], optional :param measure_processing_time: whether to measure processing time, defaults to False :type measure_processing_time: bool, optional :return: tuple of (predictions, labels, names) and processing time dictionary (if measured) @@ -269,6 +275,8 @@ def inference( end = time.perf_counter() processing_time = {"inference_n_voxelization": end - start} + if ignore_index is not None: + pred["logits"][:, ignore_index] = -1e9 pred["logits"] = torch.argmax(pred["logits"], dim=1) has_labels = pred["labels"][0] is not None diff --git a/detectionmetrics/models/lidar_torch_utils/mmdet3d.py b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py index c377e87d..2dc6bea8 100644 --- a/detectionmetrics/models/lidar_torch_utils/mmdet3d.py +++ b/detectionmetrics/models/lidar_torch_utils/mmdet3d.py @@ -91,6 +91,7 @@ def inference( sample: dict, model: torch.nn.Module, model_cfg: dict, + ignore_index: Optional[List[int]] = None, measure_processing_time: bool = False, ) -> Tuple[ Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]], Optional[dict] @@ -105,8 +106,10 @@ def inference( :type model_cfg: dict :param measure_processing_time: whether to measure processing time, defaults to False :type measure_processing_time: bool, optional + :param ignore_index: list of class indices to ignore during inference, defaults to None + :type ignore_index: Optional[List[int]], optional :return: predictions, labels (if available), sample names and optionally processing time - :rtype: Tuple[ Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str + :rtype: Tuple[ Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]], Optional[dict] ] """ single_sample = not isinstance(sample["data_samples"], list) if single_sample: @@ -133,7 +136,10 @@ def inference( preds, labels, names = ([], [], []) if has_labels else ([], None, None) for output in outputs: - preds.append(output.pred_pts_seg.pts_semantic_mask) + if ignore_index is not None: + output.pts_seg_logits.pts_seg_logits[ignore_index] = -1e9 + pred = torch.argmax(output.pts_seg_logits.pts_seg_logits, dim=0) + preds.append(pred) if has_labels: labels.append(output.gt_pts_seg.pts_semantic_mask) names.append(output.metainfo["sample_id"]) diff --git a/detectionmetrics/models/lidar_torch_utils/sphereformer.py b/detectionmetrics/models/lidar_torch_utils/sphereformer.py index 0d90cee7..226bf9f0 100644 --- a/detectionmetrics/models/lidar_torch_utils/sphereformer.py +++ b/detectionmetrics/models/lidar_torch_utils/sphereformer.py @@ -55,7 +55,7 @@ def get_sample( name: Optional[str] = None, idx: Optional[int] = None, has_intensity: bool = True, - measure_processing_time: bool = False + measure_processing_time: bool = False, ) -> Tuple[dict, Optional[dict]]: """Get sample data for mmdetection3d models @@ -123,7 +123,11 @@ def get_sample( def inference( - sample: dict, model: torch.nn.Module, model_cfg: dict, measure_processing_time: bool = False + sample: dict, + model: torch.nn.Module, + model_cfg: dict, + ignore_index: Optional[List[int]] = None, + measure_processing_time: bool = False, ) -> Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]]: """Perform inference on a sample using an mmdetection3D model @@ -135,6 +139,8 @@ def inference( :type model_cfg: dict :param measure_processing_time: whether to measure processing time, defaults to False :type measure_processing_time: bool, optional + :param ignore_index: list of class indices to ignore during inference, defaults to None + :type ignore_index: Optional[List[int]], optional :return: tuple of (predictions, labels, names) and processing time dictionary (if measured) :rtype: Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]] """ @@ -188,6 +194,8 @@ def inference( processing_time["inference"] = end - start preds = preds[inds_reconstruct, :] + if ignore_index is not None: + preds[:, ignore_index] = -1e9 preds = torch.argmax(preds, dim=1) if measure_processing_time: From 78cdbfbcc8c27e7aa81da79883b834c09b8da553 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Thu, 18 Dec 2025 11:10:12 +0100 Subject: [PATCH 32/39] Improve ontology conversion --- detectionmetrics/models/model.py | 9 +++++ detectionmetrics/models/tensorflow.py | 45 +++++++++++++++++++----- detectionmetrics/models/torch.py | 50 +++++++++++++++++++++------ examples/torch_lidar.py | 8 +++++ 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/detectionmetrics/models/model.py b/detectionmetrics/models/model.py index 7d56b539..34b4d30e 100644 --- a/detectionmetrics/models/model.py +++ b/detectionmetrics/models/model.py @@ -81,6 +81,7 @@ def eval( dataset: dm_dataset.SegmentationDataset, split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, + translations_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, results_per_sample: bool = False, ) -> pd.DataFrame: @@ -92,6 +93,8 @@ def eval( :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional + :param translations_direction: Direction of the ontology translation. Either "dataset_to_model" or "model_to_dataset", defaults to "dataset_to_model" + :type translations_direction: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. :type predictions_outdir: Optional[str], optional :param results_per_sample: Whether to store results per sample or not, defaults to False. If True, predictions_outdir must be provided. @@ -170,6 +173,7 @@ def eval( dataset: dm_dataset.ImageSegmentationDataset, split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, + translations_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, results_per_sample: bool = False, ) -> pd.DataFrame: @@ -181,6 +185,8 @@ def eval( :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional + :param translations_direction: Direction of the ontology translation. Either "dataset_to_model" or "model_to_dataset", defaults to "dataset_to_model" + :type translations_direction: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. :type predictions_outdir: Optional[str], optional :param results_per_sample: Whether to store results per sample or not, defaults to False. If True, predictions_outdir must be provided. @@ -261,6 +267,7 @@ def eval( dataset: dm_dataset.LiDARSegmentationDataset, split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, + translations_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, results_per_sample: bool = False, ) -> pd.DataFrame: @@ -272,6 +279,8 @@ def eval( :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional + :param translations_direction: Direction of the ontology translation. Either "dataset_to_model" or "model_to_dataset", defaults to "dataset_to_model" + :type translations_direction: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. :type predictions_outdir: Optional[str], optional :param results_per_sample: Whether to store results per sample or not, defaults to False. If True, predictions_outdir must be provided. diff --git a/detectionmetrics/models/tensorflow.py b/detectionmetrics/models/tensorflow.py index 55966f76..77189d0b 100644 --- a/detectionmetrics/models/tensorflow.py +++ b/detectionmetrics/models/tensorflow.py @@ -13,6 +13,8 @@ from detectionmetrics.datasets.dataset import ImageSegmentationDataset from detectionmetrics.models.model import ImageSegmentationModel +import detectionmetrics.utils.conversion as uc +import detectionmetrics.utils.io as uio import detectionmetrics.utils.metrics as um tf.config.optimizer.set_experimental_options({"layout_optimizer": False}) @@ -342,6 +344,7 @@ def eval( dataset: ImageSegmentationDataset, split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, + translations_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, results_per_sample: bool = False, ) -> pd.DataFrame: @@ -353,6 +356,8 @@ def eval( :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies :type ontology_translation: str, optional + :param translations_direction: Direction of the ontology translation. Either "dataset_to_model" or "model_to_dataset", defaults to "dataset_to_model" + :type translations_direction: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. :type predictions_outdir: Optional[str], optional :param results_per_sample: Whether to store results per sample or not, defaults to False. If True, predictions_outdir must be provided. @@ -371,8 +376,23 @@ def eval( os.makedirs(predictions_outdir, exist_ok=True) # Build a LUT for transforming ontology if needed - lut_ontology = self.get_lut_ontology(dataset.ontology, ontology_translation) - dataset_ontology = dataset.ontology + eval_ontology = self.ontology + + if ontology_translation is not None: + ontology_translation = uio.read_json(ontology_translation) + if translations_direction == "dataset_to_model": + lut_ontology = uc.get_ontology_conversion_lut( + dataset.ontology, self.ontology, ontology_translation + ) + else: + eval_ontology = dataset.ontology + lut_ontology = uc.get_ontology_conversion_lut( + self.ontology, dataset.ontology, ontology_translation + ) + else: + lut_ontology = None + + n_classes = len(eval_ontology) # Get Tensorflow dataset dataset = ImageSegmentationTensorflowDataset( @@ -381,7 +401,9 @@ def eval( crop=self.model_cfg.get("crop", None), batch_size=self.model_cfg.get("batch_size", 1), splits=[split] if isinstance(split, str) else split, - lut_ontology=lut_ontology, + lut_ontology=( + lut_ontology if translations_direction == "dataset_to_model" else None + ), normalization=self.model_cfg.get("normalization", None), keep_aspect=self.model_cfg.get("keep_aspect", False), ) @@ -389,10 +411,10 @@ def eval( # Retrieve ignored label indices ignored_label_indices = [] for ignored_class in self.model_cfg.get("ignored_classes", []): - ignored_label_indices.append(dataset_ontology[ignored_class]["idx"]) + ignored_label_indices.append(eval_ontology[ignored_class]["idx"]) # Init metrics - metrics_factory = um.MetricsFactory(self.n_classes) + metrics_factory = um.MetricsFactory(n_classes) # Evaluation loop pbar = tqdm(dataset.dataset) @@ -415,6 +437,13 @@ def eval( if valid_mask is not None: valid_mask = tf.squeeze(valid_mask, axis=3).numpy() + # Convert predictions to dataset ontology if needed + if ( + lut_ontology is not None + and translations_direction == "model_to_dataset" + ): + pred = lut_ontology[pred] + metrics_factory.update(pred, label, valid_mask) # Store predictions and results per sample if required @@ -427,16 +456,16 @@ def eval( sample_valid_mask = ( valid_mask[i] if valid_mask is not None else None ) - sample_mf = um.MetricsFactory(n_classes=self.n_classes) + sample_mf = um.MetricsFactory(n_classes) sample_mf.update(sample_pred, sample_label, sample_valid_mask) - sample_df = um.get_metrics_dataframe(sample_mf, self.ontology) + sample_df = um.get_metrics_dataframe(sample_mf, eval_ontology) sample_df.to_csv( os.path.join(predictions_outdir, f"{sample_idx}.csv") ) pred = Image.fromarray(np.squeeze(pred).astype(np.uint8)) pred.save(os.path.join(predictions_outdir, f"{sample_idx}.png")) - return um.get_metrics_dataframe(metrics_factory, self.ontology) + return um.get_metrics_dataframe(metrics_factory, eval_ontology) def get_computational_cost( self, diff --git a/detectionmetrics/models/torch.py b/detectionmetrics/models/torch.py index cca3f1f5..b51e76c1 100644 --- a/detectionmetrics/models/torch.py +++ b/detectionmetrics/models/torch.py @@ -20,6 +20,8 @@ from detectionmetrics.datasets import dataset as dm_dataset from detectionmetrics.models import model as dm_model +import detectionmetrics.utils.conversion as uc +import detectionmetrics.utils.io as uio import detectionmetrics.utils.metrics as um import detectionmetrics.utils.torch as ut @@ -404,7 +406,9 @@ def eval( os.makedirs(predictions_outdir, exist_ok=True) # Build a LUT for transforming ontology if needed - lut_ontology = self.get_lut_ontology(dataset.ontology, ontology_translation) + lut_ontology = uc.get_ontology_conversion_lut( + self.ontology, dataset.ontology, ontology_translation + ) lut_ontology = torch.tensor(lut_ontology, dtype=torch.int64).to(self.device) # Retrieve ignored label indices @@ -599,6 +603,7 @@ def predict( points_fname: str, has_intensity: bool = True, return_sample: bool = False, + ignore_index: Optional[List[int]] = None, ) -> Union[np.ndarray, Tuple[np.ndarray, Any]]: """Perform prediction for a single point cloud @@ -608,6 +613,8 @@ def predict( :type has_intensity: bool, optional :param return_sample: Whether to return the sample data along with predictions, defaults to False :type return_sample: bool, optional + :param ignore_index: List of class indices to ignore during prediction, defaults to None + :type ignore_index: Optional[List[int]], optional :return: Segmentation result as a numpy array or a tuple with the segmentation result and the input sample data :rtype: Union[np.ndarray, Tuple[np.ndarray, Any]] """ @@ -615,7 +622,7 @@ def predict( sample = self._get_sample( points_fname, self.model_cfg, has_intensity=has_intensity ) - result, _, _ = self.inference(sample, self.model, self.model_cfg) + result, _, _ = self.inference(sample, self.model, self.model_cfg, ignore_index) result = result.squeeze().cpu().numpy() if return_sample: @@ -628,6 +635,7 @@ def eval( dataset: dm_dataset.LiDARSegmentationDataset, split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, + translation_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, results_per_sample: bool = False, ) -> pd.DataFrame: @@ -638,7 +646,9 @@ def eval( :param split: Split or splits to be used from the dataset, defaults to "test" :type split: Union[str, List[str]], optional :param ontology_translation: JSON file containing translation between dataset and model output ontologies - :type ontology_translation: str, optional + :type ontology_translation: Optional[str], optional + :param translation_direction: Direction of the ontology translation, either 'dataset_to_model' or 'model_to_dataset', defaults to "dataset_to_model" + :type translation_direction: str, optional :param predictions_outdir: Directory to save predictions per sample, defaults to None. If None, predictions are not saved. :type predictions_outdir: Optional[str], optional :param results_per_sample: Whether to store results per sample or not, defaults to False. If True, predictions_outdir must be provided. @@ -657,8 +667,25 @@ def eval( os.makedirs(predictions_outdir, exist_ok=True) # Build a LUT for transforming ontology if needed - lut_ontology = self.get_lut_ontology(dataset.ontology, ontology_translation) - lut_ontology = torch.tensor(lut_ontology, dtype=torch.int64).to(self.device) + eval_ontology = self.ontology + + if ontology_translation is not None: + ontology_translation = uio.read_json(ontology_translation) + if translation_direction == "dataset_to_model": + lut_ontology = uc.get_ontology_conversion_lut( + dataset.ontology, self.ontology, ontology_translation + ) + else: + eval_ontology = dataset.ontology + lut_ontology = uc.get_ontology_conversion_lut( + self.ontology, dataset.ontology, ontology_translation + ) + + lut_ontology = torch.tensor(lut_ontology, dtype=torch.int64).to(self.device) + else: + lut_ontology = None + + n_classes = len(eval_ontology) # Retrieve ignored label indices ignored_label_indices = [] @@ -674,7 +701,7 @@ def eval( ) # Init metrics - metrics_factory = um.MetricsFactory(self.n_classes) + metrics_factory = um.MetricsFactory(n_classes) # Evaluation loop with torch.no_grad(): @@ -693,7 +720,10 @@ def eval( # Convert labels if needed if lut_ontology is not None: - label = lut_ontology[label] + if translation_direction == "dataset_to_model": + label = lut_ontology[label] + else: + pred = lut_ontology[pred] # Prepare data and update metrics factory label = label.cpu().numpy() @@ -712,12 +742,12 @@ def eval( sample_valid_mask = ( valid_mask[i] if valid_mask is not None else None ) - sample_mf = um.MetricsFactory(n_classes=self.n_classes) + sample_mf = um.MetricsFactory(n_classes) sample_mf.update( sample_pred, sample_label, sample_valid_mask ) sample_df = um.get_metrics_dataframe( - sample_mf, self.ontology + sample_mf, eval_ontology ) sample_df.to_csv( os.path.join(predictions_outdir, f"{sample_name}.csv") @@ -726,7 +756,7 @@ def eval( os.path.join(predictions_outdir, f"{sample_name}.bin") ) - return um.get_metrics_dataframe(metrics_factory, self.ontology) + return um.get_metrics_dataframe(metrics_factory, eval_ontology) def get_computational_cost( self, diff --git a/examples/torch_lidar.py b/examples/torch_lidar.py index 9ed666d8..e296f54e 100644 --- a/examples/torch_lidar.py +++ b/examples/torch_lidar.py @@ -57,6 +57,13 @@ def parse_args() -> argparse.Namespace: required=False, help="JSON file containing translation between dataset and model classes", ) + parser.add_argument( + "--translation_direction", + type=str, + choices=["dataset_to_model", "model_to_dataset"], + default="dataset_to_model", + help="Direction of the ontology translation", + ) parser.add_argument( "--predictions_outdir", type=str, @@ -85,6 +92,7 @@ def main(): dataset, split=args.split, ontology_translation=args.ontology_translation, + translation_direction=args.translation_direction, predictions_outdir=args.predictions_outdir, results_per_sample=args.predictions_outdir is not None, ) From ff0b69fb3c6376cad21d9366eef1ee808d9c55ec Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Dec 2025 10:25:56 +0100 Subject: [PATCH 33/39] Update diagram --- .../images/detectionmetricsv2_diagram.png | Bin 206022 -> 234530 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/images/detectionmetricsv2_diagram.png b/docs/assets/images/detectionmetricsv2_diagram.png index d85baea765a4fbab670cd6a6b505573e67a15e23..02929b62541c033a7b474a7a25fde1b729aff784 100644 GIT binary patch literal 234530 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 -cwz?-daN43!V~2&q&%Gd7|FE_s0FC^;IN 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 z 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

;r*JBM;qIG$>XOMfz#fd_AlzP5 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

^k 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 Date: Mon, 22 Dec 2025 11:14:09 +0100 Subject: [PATCH 34/39] Add third party notices --- README.md | 3 +- THIRD_PARTY_NOTICES.md | 178 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 THIRD_PARTY_NOTICES.md diff --git a/README.md b/README.md index e6f3619a..4abe8ca5 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,9 @@ Our previous release, ***DetectionMetrics v1***, introduced a versatile suite fo doi = {10.3390/s22124575}, } ``` + # How to Contribute _To make your first contribution, follow this [Guide](https://github.com/JdeRobot/DetectionMetrics/blob/master/CONTRIBUTING.md)._ # Acknowledgements -Utils for LiDAR segmentation, such as sampling or recentering, are based on [Open3D-ML](https://github.com/isl-org/Open3D-ML). +LiDAR segmentation support is built upon open-source work from [Open3D-ML](https://github.com/isl-org/Open3D-ML), [mmdetection3d](https://github.com/open-mmlab/mmdetection3d), [SphereFormer](https://github.com/dvlab-research/SphereFormer), and [LSK3DNet](https://github.com/FengZicai/LSK3DNet). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..934161fa --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,178 @@ +This project includes third-party software components. +The following licenses and notices apply to the indicated components. + +--- + +## Third-Party Components + +### SphereFormer +This project includes modified code derived from [SphereFormer](https://github.com/dvlab-research/SphereFormer), licensed under the Apache License, Version 2.0. + +### LSK3DNet +This project includes modified code derived from [LSK3DNet](https://github.com/FengZicai/LSK3DNet), licensed under the MIT License. + +### Open3D-ML +This project includes modified code derived from [Open3D-ML](https://github.com/isl-org/Open3D-ML), licensed under the MIT License. + +--- + +## Apache License, Version 2.0 (SphereFormer) + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity alleging that the Work + or a Contribution incorporated within the Work constitutes patent + infringement, then any patent licenses granted to You under this + License for that Work shall terminate as of the date such litigation + is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works. + + END OF TERMS AND CONDITIONS + +--- + +## MIT License (LSK3DNet) + +MIT License + +Copyright (c) 2024 Tuo Feng + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +## MIT License (Open3D-ML) + +The MIT License (MIT) + +Open3D: www.open3d.org +Copyright (c) 2020 www.open3d.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. From 27776fc91af069221093c06d5d935bea21045d52 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Dec 2025 11:14:18 +0100 Subject: [PATCH 35/39] Update CONTRIBUTING --- CONTRIBUTING.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9707c9ae..bc6ee2df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,8 +57,4 @@ Emails: - - - - From 837872605a0e0249940ff79c0e85cb6e20d2795a Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Dec 2025 14:24:18 +0100 Subject: [PATCH 36/39] Rename tensorflow module --- detectionmetrics/models/{tensorflow.py => tf_segmentation.py} | 0 examples/tensorflow_computational_cost.py | 2 +- examples/tensorflow_image.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename detectionmetrics/models/{tensorflow.py => tf_segmentation.py} (100%) diff --git a/detectionmetrics/models/tensorflow.py b/detectionmetrics/models/tf_segmentation.py similarity index 100% rename from detectionmetrics/models/tensorflow.py rename to detectionmetrics/models/tf_segmentation.py diff --git a/examples/tensorflow_computational_cost.py b/examples/tensorflow_computational_cost.py index f6cef460..5930bee1 100644 --- a/examples/tensorflow_computational_cost.py +++ b/examples/tensorflow_computational_cost.py @@ -1,6 +1,6 @@ import argparse -from detectionmetrics.models.tensorflow import TensorflowImageSegmentationModel +from detectionmetrics.models.tf_segmentation import TensorflowImageSegmentationModel def parse_args() -> argparse.Namespace: diff --git a/examples/tensorflow_image.py b/examples/tensorflow_image.py index 024910ec..ee640360 100644 --- a/examples/tensorflow_image.py +++ b/examples/tensorflow_image.py @@ -4,7 +4,7 @@ from PIL import Image from detectionmetrics.datasets.gaia import GaiaImageSegmentationDataset -from detectionmetrics.models.tensorflow import TensorflowImageSegmentationModel +from detectionmetrics.models.tf_segmentation import TensorflowImageSegmentationModel import detectionmetrics.utils.conversion as uc From 596241b3a3ef80dcc62f56c88ce720684dbb0e60 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Dec 2025 14:26:19 +0100 Subject: [PATCH 37/39] Fix LiDAR segmentation inference --- detectionmetrics/models/segmentation.py | 2 +- detectionmetrics/models/torch_segmentation.py | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/detectionmetrics/models/segmentation.py b/detectionmetrics/models/segmentation.py index af01932a..edf93e30 100644 --- a/detectionmetrics/models/segmentation.py +++ b/detectionmetrics/models/segmentation.py @@ -52,7 +52,7 @@ def predict( raise NotImplementedError @abstractmethod - def predict(self, tensor_in): + def inference(self, tensor_in): """Perform inference for a tensor :param tensor_in: Input tensor (image or point cloud) diff --git a/detectionmetrics/models/torch_segmentation.py b/detectionmetrics/models/torch_segmentation.py index 7a3cb2ce..dd180a53 100644 --- a/detectionmetrics/models/torch_segmentation.py +++ b/detectionmetrics/models/torch_segmentation.py @@ -578,20 +578,40 @@ def __init__( # Init model specific functions model_format = self.model_format.split("_")[0] - model_utils_module_str = ( - f"detectionmetrics.models.lidar_torch_utils.{model_format}" - ) + model_utils_module_str = f"detectionmetrics.models.utils.{model_format}" try: model_utils_module = importlib.import_module(model_utils_module_str) except ImportError: - raise_unknown_model_format_lidar(model_format) + raise_unknown_model_format_lidar(self.model_format) self._get_sample = model_utils_module.get_sample - self.inference = model_utils_module.inference + self._inference = model_utils_module.inference if hasattr(model_utils_module, "reset_sampler"): self._reset_sampler = model_utils_module.reset_sampler else: self._reset_sampler = None + def inference( + self, + sample: dict, + model: torch.nn.Module, + model_cfg: dict, + measure_processing_time: bool = False, + ) -> Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]]: + """Perform inference for a sample + + :param sample: Sample data + :type sample: dict + :param model: PyTorch model + :type model: torch.nn.Module + :param model_cfg: Dictionary containing model configuration + :type model_cfg: dict + :param measure_processing_time: whether to measure processing time, defaults to False + :type measure_processing_time: bool, optional + :return: tuple of (predictions, labels, names) and processing time dictionary (if measured) + :rtype: Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]] + """ + return self._inference(sample, model, model_cfg, measure_processing_time) + def predict( self, points_fname: str, From 931f5c23ac120df84ba5213a63d5690af86ae7f3 Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Dec 2025 14:26:39 +0100 Subject: [PATCH 38/39] Minor fixes --- detectionmetrics/datasets/__init__.py | 10 ++++++++-- detectionmetrics/datasets/wildscenes.py | 4 +++- detectionmetrics/models/__init__.py | 2 +- detectionmetrics/models/segmentation.py | 6 +++--- detectionmetrics/models/torch_detection.py | 6 +++--- detectionmetrics/models/torch_segmentation.py | 16 ++++++++++++---- detectionmetrics/models/utils/lsk3dnet.py | 4 ++-- detectionmetrics/models/utils/sphereformer.py | 6 ++++-- 8 files changed, 36 insertions(+), 18 deletions(-) diff --git a/detectionmetrics/datasets/__init__.py b/detectionmetrics/datasets/__init__.py index e494f858..503e3839 100644 --- a/detectionmetrics/datasets/__init__.py +++ b/detectionmetrics/datasets/__init__.py @@ -16,7 +16,11 @@ ) from detectionmetrics.datasets.rugd import RUGDImageSegmentationDataset from detectionmetrics.datasets.wildscenes import WildscenesImageSegmentationDataset -from detectionmetrics.datasets.coco import CocoDataset +try: + from detectionmetrics.datasets.coco import CocoDataset +except ImportError: + print("COCO dataset dependencies not available") + CocoDataset = None REGISTRY = { "gaia_image_segmentation": GaiaImageSegmentationDataset, @@ -29,5 +33,7 @@ "rellis3d_lidar_segmentation": Rellis3DLiDARSegmentationDataset, "rugd_image_segmentation": RUGDImageSegmentationDataset, "wildscenes_image_segmentation": WildscenesImageSegmentationDataset, - "coco_image_detection": CocoDataset, } + +if CocoDataset is not None: + REGISTRY["coco_detection"] = CocoDataset \ No newline at end of file diff --git a/detectionmetrics/datasets/wildscenes.py b/detectionmetrics/datasets/wildscenes.py index a95a21c2..2994ab5d 100644 --- a/detectionmetrics/datasets/wildscenes.py +++ b/detectionmetrics/datasets/wildscenes.py @@ -159,7 +159,9 @@ def __init__(self, dataset_dir: str, split_dir: str): super().__init__(dataset, dataset_dir, ontology) -class WildscenesLiDARSegmentationDataset(dm_dataset.LiDARSegmentationDataset): +class WildscenesLiDARSegmentationDataset( + dm_segmentation_dataset.LiDARSegmentationDataset +): """Specific class for Wildscenes-styled LiDAR segmentation datasets. All data can be downloaded from the official repo: dataset -> https://data.csiro.au/collection/csiro:61541 diff --git a/detectionmetrics/models/__init__.py b/detectionmetrics/models/__init__.py index 38b2f620..e8e4aa54 100644 --- a/detectionmetrics/models/__init__.py +++ b/detectionmetrics/models/__init__.py @@ -19,7 +19,7 @@ print("Torch detection not available") try: - from detectionmetrics.models.tensorflow import TensorflowImageSegmentationModel + from detectionmetrics.models.tf_segmentation import TensorflowImageSegmentationModel REGISTRY["tensorflow_image_segmentation"] = TensorflowImageSegmentationModel except ImportError: diff --git a/detectionmetrics/models/segmentation.py b/detectionmetrics/models/segmentation.py index edf93e30..10be5aeb 100644 --- a/detectionmetrics/models/segmentation.py +++ b/detectionmetrics/models/segmentation.py @@ -66,7 +66,7 @@ def inference(self, tensor_in): def eval( self, dataset: dm_segentation_dataset.SegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, translations_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, @@ -136,7 +136,7 @@ def predict( def eval( self, dataset: dm_segentation_dataset.ImageSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, translations_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, @@ -230,7 +230,7 @@ def predict( def eval( self, dataset: dm_segentation_dataset.LiDARSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, translations_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, diff --git a/detectionmetrics/models/torch_detection.py b/detectionmetrics/models/torch_detection.py index 5685ee7f..56aa9a09 100644 --- a/detectionmetrics/models/torch_detection.py +++ b/detectionmetrics/models/torch_detection.py @@ -268,7 +268,7 @@ def __init__( # Default to 640x640 when no resize is specified resize_height = 640 resize_width = 640 - + self.transform_input += [ transforms.Resize( size=(resize_height, resize_width), @@ -325,7 +325,7 @@ def inference(self, image: Image.Image) -> Dict[str, torch.Tensor]: def eval( self, dataset: dm_detection_dataset.ImageDetectionDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -337,7 +337,7 @@ def eval( :param dataset: Image detection dataset :type dataset: ImageDetectionDataset :param split: Dataset split(s) to evaluate - :type split: str | List[str] + :type split: Union[str, List[str]] :param ontology_translation: Optional translation for class mapping :type ontology_translation: Optional[str] :param predictions_outdir: Directory to save predictions, if desired diff --git a/detectionmetrics/models/torch_segmentation.py b/detectionmetrics/models/torch_segmentation.py index dd180a53..fc76c3a1 100644 --- a/detectionmetrics/models/torch_segmentation.py +++ b/detectionmetrics/models/torch_segmentation.py @@ -26,7 +26,13 @@ import detectionmetrics.utils.torch as ut -AVAILABLE_MODEL_FORMATS_LIDAR = ["o3d_randlanet", "o3d_kpconv", "mmdet3d"] +AVAILABLE_MODEL_FORMATS_LIDAR = [ + "o3d_randlanet", + "o3d_kpconv", + "mmdet3d", + "sphereformer", + "lsk3dnet", +] def raise_unknown_model_format_lidar(model_format: str) -> None: @@ -369,7 +375,7 @@ def inference(self, tensor_in: torch.Tensor) -> torch.Tensor: def eval( self, dataset: dm_segmentation_dataset.ImageSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, predictions_outdir: Optional[str] = None, results_per_sample: bool = False, @@ -463,7 +469,9 @@ def eval( sample_valid_mask = ( valid_mask[i] if valid_mask is not None else None ) - sample_mf = um.SegmentationMetricsFactory(n_classes=self.n_classes) + sample_mf = um.SegmentationMetricsFactory( + n_classes=self.n_classes + ) sample_mf.update( sample_pred, sample_label, sample_valid_mask ) @@ -647,7 +655,7 @@ def predict( def eval( self, dataset: dm_segmentation_dataset.LiDARSegmentationDataset, - split: str | List[str] = "test", + split: Union[str, List[str]] = "test", ontology_translation: Optional[str] = None, translation_direction: str = "dataset_to_model", predictions_outdir: Optional[str] = None, diff --git a/detectionmetrics/models/utils/lsk3dnet.py b/detectionmetrics/models/utils/lsk3dnet.py index 581c2e05..c5a99e5d 100644 --- a/detectionmetrics/models/utils/lsk3dnet.py +++ b/detectionmetrics/models/utils/lsk3dnet.py @@ -243,11 +243,11 @@ def inference( ignore_index: Optional[List[int]] = None, measure_processing_time: bool = False, ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[List[str]]]: - """Perform inference on a sample using an mmdetection3D model + """Perform inference on a sample using an LSK3DNet model :param sample: sample data dictionary :type sample: dict - :param model: mmdetection3D model + :param model: LSK3DNet model :type model: torch.nn.Module :param model_cfg: model configuration :type model_cfg: dict diff --git a/detectionmetrics/models/utils/sphereformer.py b/detectionmetrics/models/utils/sphereformer.py index 226bf9f0..1cc3a4af 100644 --- a/detectionmetrics/models/utils/sphereformer.py +++ b/detectionmetrics/models/utils/sphereformer.py @@ -33,6 +33,8 @@ def collate_fn(samples: List[dict]) -> dict: feats = torch.cat(feats) if any(label is None for label in labels): labels = None + else: + labels = torch.cat(labels) offset = torch.IntTensor(offset) inds_recons = torch.cat(inds_recons) @@ -129,11 +131,11 @@ def inference( ignore_index: Optional[List[int]] = None, measure_processing_time: bool = False, ) -> Tuple[Tuple[torch.Tensor, Optional[torch.Tensor], List[str]], Optional[dict]]: - """Perform inference on a sample using an mmdetection3D model + """Perform inference on a sample using an SphereFormer model :param sample: sample data dictionary :type sample: dict - :param model: mmdetection3D model + :param model: SphereFormer model :type model: torch.nn.Module :param model_cfg: model configuration :type model_cfg: dict From cbd3310910f0c3d240dfb32dd9bab5f93ea3666f Mon Sep 17 00:00:00 2001 From: dpascualhe Date: Mon, 22 Dec 2025 14:27:06 +0100 Subject: [PATCH 39/39] Add instructions for additiona lidar segmentation envs --- .gitignore | 1 + README.md | 3 + additional_envs/INSTRUCTIONS.md | 201 ++++++++++++++++++++ additional_envs/pyproject-lsk3dnet.toml | 46 +++++ additional_envs/pyproject-sphereformer.toml | 46 +++++ additional_envs/requirements-lsk3dnet.txt | 168 ++++++++++++++++ docs/_pages/v2/installation.md | 5 +- 7 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 additional_envs/INSTRUCTIONS.md create mode 100644 additional_envs/pyproject-lsk3dnet.toml create mode 100644 additional_envs/pyproject-sphereformer.toml create mode 100644 additional_envs/requirements-lsk3dnet.txt diff --git a/.gitignore b/.gitignore index eb3d75b0..76a13d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ dist poetry.lock local/ +third_party/ .DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 4abe8ca5..d7e0de74 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Install your deep learning framework of preference in your environment. We have If you are using LiDAR, Open3D currently requires `torch==2.2*`. +### Additional environments +Some LiDAR segmentation models, such as SphereFormer and LSK3DNet, require a dedicated installation workflow. Refer to [additional_envs/INSTRUCTIONS.md](additional_envs/INSTRUCTIONS.md) for detailed setup instructions. + # Usage DetectionMetrics can be used in three ways: through the **interactive GUI** (detection only), as a **Python library**, or via the **command-line interface** (segmentation and detection). diff --git a/additional_envs/INSTRUCTIONS.md b/additional_envs/INSTRUCTIONS.md new file mode 100644 index 00000000..c45e8d49 --- /dev/null +++ b/additional_envs/INSTRUCTIONS.md @@ -0,0 +1,201 @@ +# Additional environments + +Some LiDAR segmentation backends require a dedicated Python version and separate virtual environment. +These setups are not compatible with the default installation workflow. + +--- + +## MMDetection3D + +### Python version +- **Python 3.10** (recommended) + +### Create and activate a virtual environment +```bash +python3.10 -m venv .venv-mmdet3d +source .venv-mmdet3d/bin/activate +python -m pip install -U pip setuptools wheel +``` + +### Install dependencies (CUDA 11.7) +```bash +pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 +pip install openmim +mim install mmengine +pip install mmcv==2.1.0 -f https://download.openmmlab.com/mmcv/dist/cu117/torch2.0/index.html +mim install "mmdet>=3.0.0" +mim install "mmdet3d>=1.1.0" +``` + +### Install TorchSparse + +#### Option A (with sudo) +```bash +sudo apt update +sudo apt install -y gcc-11 g++-11 nvidia-cuda-toolkit python3.10-dev +sudo apt install -y libsparsehash-dev + +export CC=/usr/bin/gcc-11 +export CXX=/usr/bin/g++-11 +export FORCE_CUDA=1 +pip install --upgrade git+https://github.com/mit-han-lab/torchsparse.git@v1.4.0 +``` + +#### Option B (no sudo / Option A fails) + +##### 1) Install CUDA 11.7 locally +```bash +wget https://developer.download.nvidia.com/compute/cuda/11.7.0/local_installers/cuda_11.7.0_515.43.04_linux.run +chmod +x cuda_11.7.0_515.43.04_linux.run + +mkdir -p "$HOME/cuda-11.7" +./cuda_11.7.0_515.43.04_linux.run \ + --toolkit --override \ + --installpath="$HOME/cuda-11.7" + +export CUDA_HOME="$HOME/cuda-11.7" +export PATH="$CUDA_HOME/bin:$PATH" +export LD_LIBRARY_PATH="$CUDA_HOME/lib64:$LD_LIBRARY_PATH" +``` + +##### 2) Install Google's SparseHash locally +```bash +PREFIX=$HOME/local +mkdir -p "$PREFIX" && cd /tmp +wget -q https://github.com/sparsehash/sparsehash/archive/refs/tags/sparsehash-2.0.4.tar.gz +tar xzf sparsehash-2.0.4.tar.gz +cd sparsehash-sparsehash-2.0.4 +./configure --prefix="$PREFIX" +make -j"$(nproc)" && make install # headers land in $PREFIX/include/google/ +export CPLUS_INCLUDE_PATH="$PREFIX/include:$CPLUS_INCLUDE_PATH" +``` + +##### 3) Install TorchSparse +```bash +export FORCE_CUDA=1 +pip install --upgrade git+https://github.com/mit-han-lab/torchsparse.git@v1.4.0 +``` + +--- + +## SphereFormer + +### Python version +- **Python 3.7** (required) + +### Create and activate a virtual environment +```bash +python3.7 -m venv .venv-sphereformer +source .venv-sphereformer/bin/activate +python -m pip install -U pip setuptools wheel +``` + +### Install dependencies +```bash +pip install typing-extensions==4.7.1 + +pip install torch==1.8.0+cu111 torchvision==0.9.0+cu111 \ + -f https://download.pytorch.org/whl/torch_stable.html + +pip install torch_scatter==2.0.9 +pip install torch_geometric==1.7.2 +pip install spconv-cu114==2.1.25 +pip install torch_sparse==0.6.12 cumm-cu114==0.2.8 torch_cluster==1.5.9 + +pip install safetensors==0.3.3 +pip install tensorboard timm termcolor tensorboardX +``` + +### Clone SphereFormer and build its SparseTransformer +```bash +mkdir -p third_party && cd third_party +git clone https://github.com/dvlab-research/SphereFormer.git +cd SphereFormer/third_party/SparseTransformer +python setup.py install +``` + +### Switch to the SphereFormer-specific pyproject.toml and install DetectionMetrics +Run the following from the repository root: +```bash +cd ../../.. +mv pyproject.toml pyproject-core.toml +cp additional_envs/pyproject-sphereformer.toml pyproject.toml + +pip install -e . +``` + +### Add SphereFormer to PYTHONPATH +Run the following from the repository root: +```bash +export PYTHONPATH="$PYTHONPATH:$(pwd)/third_party/SphereFormer" +``` + +--- + +## LSK3DNet + +### Python version +- **Python 3.9** (required) + +Ensure `python3.9-dev` and `python3.9-distutils` are available. + +### Create and activate a virtual environment +```bash +python3.9 -m venv .venv-lsk3dnet +source .venv-lsk3dnet/bin/activate +python -m pip install -U pip setuptools wheel +``` + +### Install dependencies (CUDA 11.3) +```bash +pip install torch==1.11.0+cu113 torchvision==0.12.0+cu113 \ + --extra-index-url https://download.pytorch.org/whl/cu113 + +pip install numpy==1.23.5 +pip install -r additional_envs/requirements-lsk3dnet.txt + +pip install SharedArray==3.2.4 +pip install pybind11 +``` + +### Build LSK3DNet c_utils +```bash +mkdir -p third_party && cd third_party +git clone https://github.com/FengZicai/LSK3DNet.git + +cd LSK3DNet/c_utils +mkdir -p build && cd build + +cmake -DPYTHON_EXECUTABLE="$(which python)" \ + -Dpybind11_DIR="$(python -m pybind11 --cmakedir)" \ + .. + +make +``` + +### Switch to the LSK3DNet-specific pyproject.toml and install DetectionMetrics +Run the following from the repository root: +```bash +cd ../../../.. + +mv pyproject.toml pyproject-core.toml +cp additional_envs/pyproject-lsk3dnet.toml pyproject.toml + +pip install -e . +``` + +### Add LSK3DNet to PYTHONPATH +Run the following from the repository root: +```bash +export PYTHONPATH="$PYTHONPATH:$(pwd)/third_party/LSK3DNet:$(pwd)/third_party/LSK3DNet/c_utils/build" +``` + +--- + +## Restore the core repository configuration + +If you switched `pyproject.toml` for a backend-specific installation, restore the default setup from the repository root: + +```bash +mv pyproject-core.toml pyproject.toml +``` diff --git a/additional_envs/pyproject-lsk3dnet.toml b/additional_envs/pyproject-lsk3dnet.toml new file mode 100644 index 00000000..8a367c36 --- /dev/null +++ b/additional_envs/pyproject-lsk3dnet.toml @@ -0,0 +1,46 @@ +[tool.poetry] +name = "detectionmetrics-lsk3dnet" +version = "0.0.0" +description = "LSK3DNet-compatible version of DetectionMetrics" +authors = ["JdeRobot", "d.pascualhe "] +readme = "README.md" +license = "LICENSE" +packages = [ + { include = "detectionmetrics" } +] + +[tool.poetry.dependencies] +python = "^3.9" +tqdm = "^4.65.0" +pandas = "^2.2.3" +PyYAML = "^6.0.2" +pyarrow = "^18.0.0" +pillow = "^11.0.0" +numpy = "1.23.5" +opencv-python-headless = "^4.10.0.84" +scikit-learn = "^1.6.0" +open3d = "^0.19.0" +addict = "^2.4.0" +matplotlib = "^3.6.0" +click = "^8.1.8" +tensorboard = "^2.18.0" + +[tool.poetry.group.dev.dependencies] +black = "^24.10.0" +pylint = "^3.3.1" +ipykernel = "^6.29.5" + +[tool.poetry.group.docs.dependencies] +sphinx = "^8.1.3" +sphinx-rtd-theme = "^3.0.2" + +[tool.poetry.group.test.dependencies] +pytest = "^8.0.0" + +[tool.poetry.scripts] +dm_evaluate = "detectionmetrics.cli.evaluate:evaluate" +dm_batch = "detectionmetrics.cli.batch:batch" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/additional_envs/pyproject-sphereformer.toml b/additional_envs/pyproject-sphereformer.toml new file mode 100644 index 00000000..ba668b94 --- /dev/null +++ b/additional_envs/pyproject-sphereformer.toml @@ -0,0 +1,46 @@ +[tool.poetry] +name = "detectionmetrics-sphereformer" +version = "0.0.0" +description = "SphereFormer-compatible version of DetectionMetrics" +authors = ["JdeRobot", "d.pascualhe "] +readme = "README.md" +license = "LICENSE" +packages = [ + { include = "detectionmetrics" } +] + +[tool.poetry.dependencies] +python = "^3.7" +tqdm = "^4.65.0" +pandas = "^1.3.5" +PyYAML = "^5.4.1" +pyarrow = "^12.0.1" +pillow = "9.5.0" +numpy = "1.21.6" +opencv-python-headless = "^4.5.5.64" +scikit-learn = "^1.0.2" +open3d = "^0.13.0" +addict = "^2.4.0" +matplotlib = "^3.5.0" +click = "^8.0.4" +tensorboard = "^2.6.0" + +[tool.poetry.group.dev.dependencies] +black = "^22.3.0" +pylint = "^2.15.0" +ipykernel = "^6.9.1" + +[tool.poetry.group.docs.dependencies] +sphinx = "^4.5.0" +sphinx-rtd-theme = "^1.0.0" + +[tool.poetry.group.test.dependencies] +pytest = "^6.2.5" + +[tool.poetry.scripts] +dm_evaluate = "detectionmetrics.cli.evaluate:evaluate" +dm_batch = "detectionmetrics.cli.batch:batch" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/additional_envs/requirements-lsk3dnet.txt b/additional_envs/requirements-lsk3dnet.txt new file mode 100644 index 00000000..a4816cda --- /dev/null +++ b/additional_envs/requirements-lsk3dnet.txt @@ -0,0 +1,168 @@ +absl-py==1.4.0 +addict==2.4.0 +aiohttp==3.8.3 +aiosignal==1.3.1 +anyio==3.6.2 +argon2-cffi==21.3.0 +argon2-cffi-bindings==21.2.0 +async-timeout==4.0.2 +asynctest==0.13.0 +attrs==22.2.0 +backcall==0.2.0 +beautifulsoup4==4.11.1 +bleach==5.0.1 +cachetools==5.3.0 +ccimport==0.3.7 +certifi==2024.8.30 +cffi==1.15.1 +click==8.1.3 +ConfigArgParse==1.5.3 +cumm-cu113==0.2.9 +cycler==0.11.0 +Cython==0.29.32 +dash==2.8.1 +dash-core-components==2.0.0 +dash-html-components==2.0.0 +dash-table==5.0.0 +debugpy==1.6.4 +decorator==5.1.1 +defusedxml==0.7.1 +descartes==1.1.0 +easydict==1.10 +entrypoints==0.4 +fastjsonschema==2.16.3 +filelock==3.9.1 +fire==0.5.0 +Flask==2.2.3 +fonttools==4.38.0 +frozenlist==1.3.3 +fsspec==2022.11.0 +funcy==2.0 +future==0.18.3 +gensim==4.2.0 +google-auth==2.17.1 +google-auth-oauthlib==0.4.6 +grpcio==1.53.0 +huggingface-hub==0.13.2 +idna==3.4 +imageio==2.27.0 +importlib-metadata==5.2.0 +importlib-resources==5.10.2 +ipykernel==6.16.2 +ipython==7.34.0 +ipython-genutils==0.2.0 +ipywidgets==8.0.4 +itsdangerous==2.1.2 +jedi==0.18.2 +Jinja2==3.1.2 +joblib==1.2.0 +jsonschema==4.17.3 +jupyter==1.0.0 +jupyter-console==6.4.4 +jupyter-server==1.23.5 +jupyter_client==7.4.8 +jupyter_core==4.12.0 +jupyterlab-pygments==0.2.2 +jupyterlab-widgets==3.0.5 +kiwisolver==1.4.4 +lark==1.1.5 +lightning-utilities==0.5.0 +llvmlite==0.39.1 +Markdown==3.4.3 +MarkupSafe==2.1.2 +matplotlib==3.5.3 +matplotlib-inline==0.1.6 +mistune==2.0.4 +multidict==6.0.4 +nbclassic==0.4.8 +nbclient==0.7.2 +nbconvert==7.2.8 +nbformat==5.5.0 +nest-asyncio==1.5.6 +networkx==2.6.3 +ninja==1.11.1 +nltk==3.8.1 +notebook==6.5.2 +notebook_shim==0.2.2 +numba==0.56.4 +numexpr==2.8.4 +nuscenes-devkit==1.1.9 +oauthlib==3.2.2 +open3d==0.16.0 +opencv-python==4.7.0.68 +packaging==23.0 +pandas==1.3.5 +pandocfilters==1.5.0 +parso==0.8.3 +pccm==0.2.21 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.4.0 +pkgutil_resolve_name==1.3.10 +plotly==5.13.1 +plyfile==0.8.1 +portalocker==2.6.0 +prometheus-client==0.15.0 +prompt-toolkit==3.0.36 +protobuf==3.20.1 +psutil==5.9.4 +ptyprocess==0.7.0 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pybind11==2.10.2 +pycocotools==2.0.6 +pycparser==2.21 +pyDeprecate==0.3.0 +Pygments==2.14.0 +pyparsing==3.0.9 +pyquaternion==0.9.9 +pyrsistent==0.19.3 +python-dateutil==2.8.2 +pytz==2022.7 +PyWavelets==1.3.0 +PyYAML==6.0 +pyzmq==24.0.1 +qtconsole==5.4.0 +QtPy==2.3.0 +requests==2.28.2 +requests-oauthlib==1.3.1 +rsa==4.9 +scikit-image==0.19.3 +scikit-learn==1.0.2 +scipy==1.7.3 +seaborn==0.12.2 +Send2Trash==1.8.0 +shapely==2.0.0 +six==1.16.0 +smart-open==6.3.0 +sniffio==1.3.0 +soupsieve==2.3.2.post1 +spconv-cu113==2.1.21 +strictyaml==1.6.2 +tenacity==8.2.2 +tensorboard==2.11.2 +tensorboard-data-server==0.6.1 +tensorboard-plugin-wit==1.8.1 +tensorboardX==2.5.1 +termcolor==2.1.1 +terminado==0.17.1 +threadpoolctl==3.1.0 +tifffile==2021.11.2 +timm==0.6.12 +tinycss2==1.2.1 +torch-scatter==2.1.0 +torchmetrics==0.5.0 +tornado==6.2 +tqdm==4.64.1 +traitlets==5.9.0 +trimesh==3.21.3 +typing_extensions==4.4.0 +urllib3==1.26.15 +wcwidth==0.2.5 +webencodings==0.5.1 +websocket-client==1.4.2 +Werkzeug==2.2.3 +widgetsnbextension==4.0.5 +yacs==0.1.8 +yarl==1.8.2 +zipp==3.11.0 \ No newline at end of file diff --git a/docs/_pages/v2/installation.md b/docs/_pages/v2/installation.md index 2169365b..a5a1f12a 100644 --- a/docs/_pages/v2/installation.md +++ b/docs/_pages/v2/installation.md @@ -49,4 +49,7 @@ Install your deep learning framework of preference in your environment. We have If you are using LiDAR, Open3D currently requires `torch==2.2*`. -And it's done! You can check the `examples` directory for inspiration and run some of the scripts provided either by activating the created environment using `poetry shell` or directly running `poetry run python examples/`. \ No newline at end of file +And it's done! You can check the `examples` directory for inspiration and run some of the scripts provided either by activating the created environment using `poetry shell` or directly running `poetry run python examples/`. + +### Additional environments +Some LiDAR segmentation models, such as SphereFormer and LSK3DNet, require a dedicated installation workflow. Refer to [additional_envs/INSTRUCTIONS.md](additional_envs/INSTRUCTIONS.md) for detailed setup instructions. \ No newline at end of file