From 8f7f6ac510107c243d93bc3258c6b4aa1aadb952 Mon Sep 17 00:00:00 2001 From: MARC YERANOSYAN <25641544+CEZERT@users.noreply.github.com> Date: Mon, 28 Apr 2025 03:02:20 +0200 Subject: [PATCH 1/3] Fix lidar_hd_pre_transform to support missing RGB channels Fix lidar_hd_pre_transform to support missing RGB channels + update devcontainer shm-size + add .vscode --- .devcontainer/devcontainer.json | 16 +++ myria3d/pctl/points_pre_transform/lidar_hd.py | 104 ++++++++++-------- 2 files changed, 73 insertions(+), 47 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..435ee533 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,16 @@ +{ + "name": "Myria3D", + "build": { + "dockerfile": "../Dockerfile", + "context": ".." + }, + "settings": {}, + "extensions": [ + "ms-python.python", + "ms-azuretools.vscode-docker" + ], + "runArgs": ["--gpus", "all", "--shm-size=8g"], + "remoteUser": "root" +} + + \ No newline at end of file diff --git a/myria3d/pctl/points_pre_transform/lidar_hd.py b/myria3d/pctl/points_pre_transform/lidar_hd.py index dcd7e4ad..02e091fa 100644 --- a/myria3d/pctl/points_pre_transform/lidar_hd.py +++ b/myria3d/pctl/points_pre_transform/lidar_hd.py @@ -1,77 +1,87 @@ -# function to turn points loaded via pdal into a pyg Data object, with additional channels import numpy as np from torch_geometric.data import Data +from numpy.lib.recfunctions import append_fields + COLORS_NORMALIZATION_MAX_VALUE = 255.0 * 256.0 RETURN_NUMBER_NORMALIZATION_MAX_VALUE = 7.0 - def lidar_hd_pre_transform(points): """Turn pdal points into torch-geometric Data object. - Builds a composite (average) color channel on the fly. Calculate NDVI on the fly. + Builds a composite (average) color channel on the fly. Calculate NDVI on the fly. Args: - las_filepath (str): path to the LAS file. + points (np.ndarray): points loaded via PDAL Returns: - Data: the point cloud formatted for later deep learning training. - + Data: the point cloud formatted for deep learning training. """ - # Positions and base features + # Positions pos = np.asarray([points["X"], points["Y"], points["Z"]], dtype=np.float32).transpose() + # normalization occluded_points = points["ReturnNumber"] > 1 - points["ReturnNumber"] = (points["ReturnNumber"]) / (RETURN_NUMBER_NORMALIZATION_MAX_VALUE) - points["NumberOfReturns"] = (points["NumberOfReturns"]) / ( - RETURN_NUMBER_NORMALIZATION_MAX_VALUE - ) + points["ReturnNumber"] = (points["ReturnNumber"]) / RETURN_NUMBER_NORMALIZATION_MAX_VALUE + points["NumberOfReturns"] = (points["NumberOfReturns"]) / RETURN_NUMBER_NORMALIZATION_MAX_VALUE + + # Ensure all color fields exist, even if missing (filled with 0) + for color in ["Red", "Green", "Blue", "Infrared"]: + if color not in points.dtype.names: + print(f"Color channel {color} not found. Creating fake {color} filled with 0.") + fake_color = np.zeros(points.shape[0], dtype=np.float32) + points = append_fields(points, color, fake_color, dtypes=np.float32, usemask=False) + # Normalize colors if available + available_colors = [] for color in ["Red", "Green", "Blue", "Infrared"]: - assert points[color].max() <= COLORS_NORMALIZATION_MAX_VALUE - points[color][:] = points[color] / COLORS_NORMALIZATION_MAX_VALUE - points[color][occluded_points] = 0.0 - - # Additional features : - # Average color, that will be normalized on the fly based on single-sample - rgb_avg = ( - np.asarray([points["Red"], points["Green"], points["Blue"]], dtype=np.float32) - .transpose() - .mean(axis=1) - ) + if color in points.dtype.names: + assert points[color].max() <= COLORS_NORMALIZATION_MAX_VALUE, f"{color} max too high!" + points[color][:] = points[color] / COLORS_NORMALIZATION_MAX_VALUE + points[color][occluded_points] = 0.0 + available_colors.append(color) + else: + print(f"Warning: {color} channel not found. Skipping.") + + # Additional features + rgb_avg = np.zeros(points.shape[0], dtype=np.float32) + if all(c in points.dtype.names for c in ["Red", "Green", "Blue"]): + rgb_avg = ( + np.asarray([points["Red"], points["Green"], points["Blue"]], dtype=np.float32) + .transpose() + .mean(axis=1) + ) # NDVI - ndvi = (points["Infrared"] - points["Red"]) / (points["Infrared"] + points["Red"] + 10**-6) - - # todo - x = np.stack( - [ - points[name] - for name in [ - "Intensity", - "ReturnNumber", - "NumberOfReturns", - "Red", - "Green", - "Blue", - "Infrared", - ] - ] - + [rgb_avg, ndvi], - axis=0, - ).transpose() + ndvi = np.zeros(points.shape[0], dtype=np.float32) + if "Infrared" in points.dtype.names and "Red" in points.dtype.names: + ndvi = (points["Infrared"] - points["Red"]) / (points["Infrared"] + points["Red"] + 1e-6) + + # Features list: dynamically adapt based on what exists + x_list = [ + points["Intensity"], + points["ReturnNumber"], + points["NumberOfReturns"], + ] + x_features_names = [ "Intensity", "ReturnNumber", "NumberOfReturns", - "Red", - "Green", - "Blue", - "Infrared", - "rgb_avg", - "ndvi", ] + + for color in ["Red", "Green", "Blue", "Infrared"]: + if color in points.dtype.names: + x_list.append(points[color]) + x_features_names.append(color) + + # Always add computed features + x_list += [rgb_avg, ndvi] + x_features_names += ["rgb_avg", "ndvi"] + + x = np.stack(x_list, axis=0).transpose() + y = points["Classification"] data = Data(pos=pos, x=x, y=y, x_features_names=x_features_names) From 7be21bbd33b4070c60334d5c0d3a03de69ad1e16 Mon Sep 17 00:00:00 2001 From: Lea Vauchier Date: Tue, 3 Jun 2025 14:46:48 +0200 Subject: [PATCH 2/3] Run black on updated file --- myria3d/pctl/points_pre_transform/lidar_hd.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/myria3d/pctl/points_pre_transform/lidar_hd.py b/myria3d/pctl/points_pre_transform/lidar_hd.py index 02e091fa..c0501ea7 100644 --- a/myria3d/pctl/points_pre_transform/lidar_hd.py +++ b/myria3d/pctl/points_pre_transform/lidar_hd.py @@ -1,11 +1,11 @@ import numpy as np -from torch_geometric.data import Data from numpy.lib.recfunctions import append_fields - +from torch_geometric.data import Data COLORS_NORMALIZATION_MAX_VALUE = 255.0 * 256.0 RETURN_NUMBER_NORMALIZATION_MAX_VALUE = 7.0 + def lidar_hd_pre_transform(points): """Turn pdal points into torch-geometric Data object. @@ -25,7 +25,7 @@ def lidar_hd_pre_transform(points): points["ReturnNumber"] = (points["ReturnNumber"]) / RETURN_NUMBER_NORMALIZATION_MAX_VALUE points["NumberOfReturns"] = (points["NumberOfReturns"]) / RETURN_NUMBER_NORMALIZATION_MAX_VALUE - + # Ensure all color fields exist, even if missing (filled with 0) for color in ["Red", "Green", "Blue", "Infrared"]: if color not in points.dtype.names: From 11e6174c36b80e3f0f1b29f06ce917edb4c2ed7f Mon Sep 17 00:00:00 2001 From: Lea Vauchier Date: Tue, 3 Jun 2025 17:48:31 +0200 Subject: [PATCH 3/3] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24613047..c4e58086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # CHANGELOG +- Fix lidar_hd_pre_transform to support missing RGB channels #138 (contrib from @CEZERT) - Add a github action workflow to run a trained model on the lidar-prod thresholds optimisation dataset (in order to automate thresholds optimization)