diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 7371105a..e19579b0 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -61,7 +61,7 @@ jobs: myria3d python run.py --config-path /inputs/ - --config-name proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.4.0 + --config-name proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.5.0 predict.ckpt_path=/inputs/proto151_V2.0_epoch_100_Myria3DV3.1.0.ckpt predict.src_las=/inputs/792000_6272000_subset_buildings.las predict.output_dir=/outputs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 189e2d73..cf59a674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # main +### 3.5.0 +- Abandon of option to get circular patches since it was never used. + ### 3.4.9 - Support edge-case where source LAZ has no valid subtile (i.e. pre_filter=False for all candidate subtiles) during hdf5 creation diff --git a/configs/datamodule/hdf5_datamodule.yaml b/configs/datamodule/hdf5_datamodule.yaml index c39147a3..10fac67f 100755 --- a/configs/datamodule/hdf5_datamodule.yaml +++ b/configs/datamodule/hdf5_datamodule.yaml @@ -19,7 +19,6 @@ pre_filter: tile_width: 1000 subtile_width: 50 -subtile_shape: "square" # "square" or "disk" subtile_overlap_train: 0 subtile_overlap_predict: "${predict.subtile_overlap}" diff --git a/docs/source/apidoc/default_config.yml b/docs/source/apidoc/default_config.yml index acee234b..a6530417 100644 --- a/docs/source/apidoc/default_config.yml +++ b/docs/source/apidoc/default_config.yml @@ -119,7 +119,6 @@ datamodule: min_num_nodes: 50 tile_width: 1000 subtile_width: 50 - subtile_shape: square subtile_overlap_train: 0 subtile_overlap_predict: ${predict.subtile_overlap} batch_size: 2 diff --git a/myria3d/pctl/datamodule/hdf5.py b/myria3d/pctl/datamodule/hdf5.py index 3f559ca3..edeb0d1b 100644 --- a/myria3d/pctl/datamodule/hdf5.py +++ b/myria3d/pctl/datamodule/hdf5.py @@ -11,7 +11,6 @@ from myria3d.pctl.dataset.hdf5 import HDF5Dataset from myria3d.pctl.dataset.iterable import InferenceDataset from myria3d.pctl.dataset.utils import ( - SHAPE_TYPE, get_las_paths_by_split_dict, pre_filter_below_n_points, ) @@ -34,13 +33,13 @@ def __init__( pre_filter: Optional[Callable[[Data], bool]] = pre_filter_below_n_points, tile_width: Number = 1000, subtile_width: Number = 50, - subtile_shape: SHAPE_TYPE = "square", subtile_overlap_train: Number = 0, subtile_overlap_predict: Number = 0, batch_size: int = 12, num_workers: int = 1, prefetch_factor: int = 2, transforms: Optional[Dict[str, TRANSFORMS_LIST]] = None, + **kwargs ): self.split_csv_path = split_csv_path self.data_dir = data_dir @@ -53,7 +52,6 @@ def __init__( self.tile_width = tile_width self.subtile_width = subtile_width - self.subtile_shape = subtile_shape self.subtile_overlap_train = subtile_overlap_train self.subtile_overlap_predict = subtile_overlap_predict @@ -62,32 +60,50 @@ def __init__( self.prefetch_factor = prefetch_factor t = transforms - self.preparation_train_transform: TRANSFORMS_LIST = t.get("preparations_train_list", []) - self.preparation_eval_transform: TRANSFORMS_LIST = t.get("preparations_eval_list", []) - self.preparation_predict_transform: TRANSFORMS_LIST = t.get("preparations_predict_list", []) + self.preparation_train_transform: TRANSFORMS_LIST = t.get( + "preparations_train_list", [] + ) + self.preparation_eval_transform: TRANSFORMS_LIST = t.get( + "preparations_eval_list", [] + ) + self.preparation_predict_transform: TRANSFORMS_LIST = t.get( + "preparations_predict_list", [] + ) self.augmentation_transform: TRANSFORMS_LIST = t.get("augmentations_list", []) self.normalization_transform: TRANSFORMS_LIST = t.get("normalizations_list", []) @property def train_transform(self) -> CustomCompose: - return CustomCompose(self.preparation_train_transform + self.normalization_transform + self.augmentation_transform) + return CustomCompose( + self.preparation_train_transform + + self.normalization_transform + + self.augmentation_transform + ) @property def eval_transform(self) -> CustomCompose: - return CustomCompose(self.preparation_eval_transform + self.normalization_transform) + return CustomCompose( + self.preparation_eval_transform + self.normalization_transform + ) @property def predict_transform(self) -> CustomCompose: - return CustomCompose(self.preparation_predict_transform + self.normalization_transform) + return CustomCompose( + self.preparation_predict_transform + self.normalization_transform + ) def prepare_data(self, stage: Optional[str] = None): """Prepare dataset containing train, val, test data.""" if stage in ["fit", "test"] or stage is None: if self.split_csv_path and self.data_dir: - las_paths_by_split_dict = get_las_paths_by_split_dict(self.data_dir, self.split_csv_path) + las_paths_by_split_dict = get_las_paths_by_split_dict( + self.data_dir, self.split_csv_path + ) else: - log.warning("cfg.data_dir and cfg.split_csv_path are both null. Precomputed HDF5 dataset is used.") + log.warning( + "cfg.data_dir and cfg.split_csv_path are both null. Precomputed HDF5 dataset is used." + ) las_paths_by_split_dict = None # Create the dataset in prepare_data, so that it is done one a single GPU. self.las_paths_by_split_dict = las_paths_by_split_dict @@ -124,7 +140,6 @@ def dataset(self) -> HDF5Dataset: tile_width=self.tile_width, subtile_width=self.subtile_width, subtile_overlap_train=self.subtile_overlap_train, - subtile_shape=self.subtile_shape, pre_filter=self.pre_filter, train_transform=self.train_transform, eval_transform=self.eval_transform, @@ -164,7 +179,6 @@ def _set_predict_data(self, las_file_to_predict): transform=self.predict_transform, tile_width=self.tile_width, subtile_width=self.subtile_width, - subtile_shape=self.subtile_shape, subtile_overlap=self.subtile_overlap_predict, ) diff --git a/myria3d/pctl/dataset/hdf5.py b/myria3d/pctl/dataset/hdf5.py index f9b1dbc6..0fe01481 100644 --- a/myria3d/pctl/dataset/hdf5.py +++ b/myria3d/pctl/dataset/hdf5.py @@ -34,7 +34,6 @@ def __init__( tile_width: Number = 1000, subtile_width: Number = 50, subtile_overlap_train: Number = 0, - subtile_shape: SHAPE_TYPE = "square", pre_filter=pre_filter_below_n_points, train_transform: List[Callable] = None, eval_transform: List[Callable] = None, @@ -48,7 +47,6 @@ def __init__( points_pre_transform (Callable): Function to turn pdal points into a pyg Data object. tile_width (Number, optional): width of a LAS tile. Defaults to 1000. subtile_width (Number, optional): effective width of a subtile (i.e. receptive field). Defaults to 50. - subtile_shape (SHAPE_TYPE, optional): Shape of subtile could be either "square" or "disk". Defaults to "square". subtile_overlap_train (Number, optional): Overlap for data augmentation of train set. Defaults to 0. pre_filter (_type_, optional): Function to filter out specific subtiles. Defaults to None. train_transform (List[Callable], optional): Transforms to apply to a sample for training. Defaults to None. @@ -64,7 +62,6 @@ def __init__( self.tile_width = tile_width self.subtile_width = subtile_width self.subtile_overlap_train = subtile_overlap_train - self.subtile_shape = subtile_shape self.hdf5_file_path = hdf5_file_path @@ -83,7 +80,6 @@ def __init__( hdf5_file_path, tile_width, subtile_width, - subtile_shape, pre_filter, subtile_overlap_train, points_pre_transform, @@ -198,7 +194,6 @@ def create_hdf5( hdf5_file_path: str, tile_width: Number = 1000, subtile_width: Number = 50, - subtile_shape: SHAPE_TYPE = "square", pre_filter: Optional[Callable[[Data], bool]] = pre_filter_below_n_points, subtile_overlap_train: Number = 0, points_pre_transform: Callable = lidar_hd_pre_transform, @@ -215,7 +210,6 @@ def create_hdf5( hdf5_file_path (str): path to HDF5 dataset, tile_width (Number, optional): width of a LAS tile. 1000 by default, subtile_width: (Number, optional): effective width of a subtile (i.e. receptive field). 50 by default, - subtile_shape (SHAPE_TYPE, optional): Shape of subtile could be either "square" or "disk". "square" by default , pre_filter: Function to filter out specific subtiles. "pre_filter_below_n_points" by default, subtile_overlap_train (Number, optional): Overlap for data augmentation of train set. 0 by default, points_pre_transform (Callable): Function to turn pdal points into a pyg Data object. @@ -246,7 +240,6 @@ def create_hdf5( las_path, tile_width, subtile_width, - subtile_shape, subtile_overlap, ) ): diff --git a/myria3d/pctl/dataset/iterable.py b/myria3d/pctl/dataset/iterable.py index a6e6ea0f..d230043a 100644 --- a/myria3d/pctl/dataset/iterable.py +++ b/myria3d/pctl/dataset/iterable.py @@ -7,7 +7,6 @@ from torch_geometric.data import Data from myria3d.pctl.dataset.utils import ( - SHAPE_TYPE, pre_filter_below_n_points, split_cloud_into_samples, ) @@ -26,7 +25,6 @@ def __init__( tile_width: Number = 1000, subtile_width: Number = 50, subtile_overlap: Number = 0, - subtile_shape: SHAPE_TYPE = "square", ): self.las_file = las_file @@ -36,7 +34,6 @@ def __init__( self.tile_width = tile_width self.subtile_width = subtile_width - self.subtile_shape = subtile_shape self.subtile_overlap = subtile_overlap def __iter__(self): @@ -48,7 +45,6 @@ def get_iterator(self): self.las_file, self.tile_width, self.subtile_width, - self.subtile_shape, self.subtile_overlap, ): sample_data = self.points_pre_transform(sample_points) diff --git a/myria3d/pctl/dataset/utils.py b/myria3d/pctl/dataset/utils.py index 7e62e958..ab7dc6fb 100644 --- a/myria3d/pctl/dataset/utils.py +++ b/myria3d/pctl/dataset/utils.py @@ -13,7 +13,6 @@ from shapely.geometry import Point SPLIT_TYPE = Union[Literal["train"], Literal["val"], Literal["test"]] -SHAPE_TYPE = Union[Literal["disk"], Literal["square"]] LAS_PATHS_BY_SPLIT_DICT_TYPE = Dict[SPLIT_TYPE, List[str]] # commons @@ -31,7 +30,9 @@ def find_file_in_dir(data_dir: str, basename: str) -> str: return files[0] -def get_mosaic_of_centers(tile_width: Number, subtile_width: Number, subtile_overlap: Number = 0): +def get_mosaic_of_centers( + tile_width: Number, subtile_width: Number, subtile_overlap: Number = 0 +): if subtile_overlap < 0: raise ValueError("datamodule.subtile_overlap must be positive.") @@ -61,7 +62,9 @@ def pdal_read_las_array(las_path: str): def pdal_read_las_array_as_float32(las_path: str): """Read LAS as a a named array, casted to floats.""" arr = pdal_read_las_array(las_path) - all_floats = np.dtype({"names": arr.dtype.names, "formats": ["f4"] * len(arr.dtype.names)}) + all_floats = np.dtype( + {"names": arr.dtype.names, "formats": ["f4"] * len(arr.dtype.names)} + ) return arr.astype(all_floats) @@ -105,7 +108,6 @@ def split_cloud_into_samples( las_path: str, tile_width: Number, subtile_width: Number, - shape: SHAPE_TYPE, subtile_overlap: Number = 0, ): """Split LAS point cloud into samples. @@ -113,8 +115,7 @@ def split_cloud_into_samples( Args: las_path (str): path to raw LAS file tile_width (Number): width of input LAS file - subtile_width (Number): width of receptive field ; may be increased for coverage in case of disk shape. - shape: "disk" or "square" + subtile_width (Number): width of receptive field. subtile_overlap (Number, optional): overlap between adjacent tiles. Defaults to 0. Yields: @@ -122,17 +123,16 @@ def split_cloud_into_samples( """ points = pdal_read_las_array_as_float32(las_path) - pos = np.asarray([points["X"], points["Y"], points["Z"]], dtype=np.float32).transpose() + pos = np.asarray( + [points["X"], points["Y"], points["Z"]], dtype=np.float32 + ).transpose() kd_tree = cKDTree(pos[:, :2] - pos[:, :2].min(axis=0)) - XYs = get_mosaic_of_centers(tile_width, subtile_width, subtile_overlap=subtile_overlap) + XYs = get_mosaic_of_centers( + tile_width, subtile_width, subtile_overlap=subtile_overlap + ) for center in XYs: radius = subtile_width // 2 # Square receptive field. minkowski_p = np.inf - if shape == "disk": - # Disk receptive field. - # Adapt radius to have complete coverage of the data, with a slight overlap between samples. - minkowski_p = 2 - radius = radius * math.sqrt(2) sample_idx = np.array(kd_tree.query_ball_point(center, r=radius, p=minkowski_p)) if not len(sample_idx): # no points in this receptive fields @@ -170,7 +170,9 @@ def get_las_paths_by_split_dict( for phase in ["train", "val", "test"]: basenames = split_df[split_df.split == phase].basename.tolist() # Reminder: an explicit data structure with ./val, ./train, ./test subfolder is required. - las_paths_by_split_dict[phase] = [str(Path(data_dir) / phase / b) for b in basenames] + las_paths_by_split_dict[phase] = [ + str(Path(data_dir) / phase / b) for b in basenames + ] if not las_paths_by_split_dict: raise FileNotFoundError( diff --git a/package_metadata.yaml b/package_metadata.yaml index 4898c136..a4dc0530 100644 --- a/package_metadata.yaml +++ b/package_metadata.yaml @@ -1,4 +1,4 @@ -__version__: "3.4.9" +__version__: "3.5.0" __name__: "myria3d" __url__: "https://github.com/IGNF/myria3d" __description__: "Deep Learning for the Semantic Segmentation of Aerial Lidar Point Clouds" diff --git a/run.py b/run.py index 3d56cad1..2327de83 100755 --- a/run.py +++ b/run.py @@ -22,7 +22,7 @@ TASK_NAME_DETECTION_STRING = "task.task_name=" DEFAULT_DIRECTORY = "trained_model_assets/" -DEFAULT_CONFIG_FILE = "proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.4.0.yaml" +DEFAULT_CONFIG_FILE = "proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.5.0.yaml" DEFAULT_CHECKPOINT = "proto151_V2.0_epoch_100_Myria3DV3.1.0.ckpt" DEFAULT_ENV = "placeholder.env" @@ -96,7 +96,6 @@ def launch_hdf5(config: DictConfig): hdf5_file_path=config.datamodule.get("hdf5_file_path"), tile_width=config.datamodule.get("tile_width"), subtile_width=config.datamodule.get("subtile_width"), - subtile_shape=config.datamodule.get("subtile_shape"), pre_filter=hydra.utils.instantiate(config.datamodule.get("pre_filter")), subtile_overlap_train=config.datamodule.get("subtile_overlap_train"), points_pre_transform=hydra.utils.instantiate( @@ -114,7 +113,11 @@ def launch_hdf5(config: DictConfig): log.info(f"Task: {task_name}") - if task_name in [TASK_NAMES.FIT.value, TASK_NAMES.TEST.value, TASK_NAMES.FINETUNE.value]: + if task_name in [ + TASK_NAMES.FIT.value, + TASK_NAMES.TEST.value, + TASK_NAMES.FINETUNE.value, + ]: # load environment variables from `.env` file if it exists # recursively searches for `.env` in all folders starting from work dir dotenv.load_dotenv(override=True) diff --git a/trained_model_assets/proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.4.0.yaml b/trained_model_assets/proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.5.0.yaml similarity index 99% rename from trained_model_assets/proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.4.0.yaml rename to trained_model_assets/proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.5.0.yaml index acd15868..009293d6 100644 --- a/trained_model_assets/proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.4.0.yaml +++ b/trained_model_assets/proto151_V2.0_epoch_100_Myria3DV3.1.0_predict_config_V3.5.0.yaml @@ -132,7 +132,6 @@ datamodule: min_num_nodes: 1 tile_width: 1000 subtile_width: 50 - subtile_shape: square subtile_overlap_train: 0 subtile_overlap_predict: ${predict.subtile_overlap} batch_size: 10