From fc04c28d23bf11ae10225cf98b25229f04ea2fdd Mon Sep 17 00:00:00 2001 From: Lea Vauchier Date: Mon, 2 Jun 2025 17:33:55 +0200 Subject: [PATCH] Convert paths from shapefile using mount points --- CHANGELOG.md | 3 +- README.md | 45 ++++--- configs/configs_patchwork.yaml | 5 + patchwork/patchwork.py | 5 +- patchwork/path_manipulation.py | 38 ++++++ patchwork/shapefile_data_extraction.py | 26 +++- test/configs/config_test_mount_points.yaml | 63 ++++++++++ .../patchwork_geometries.dbf | Bin 0 -> 2950 bytes .../patchwork_geometries.shp | Bin 0 -> 11476 bytes .../patchwork_geometries.shx | Bin 0 -> 132 bytes .../patchwork_geometries.dbf | Bin 0 -> 2950 bytes .../patchwork_geometries.shp | Bin 0 -> 11476 bytes .../patchwork_geometries.shx | Bin 0 -> 132 bytes test/test_indices_map.py | 1 - test/test_patchwork.py | 77 ++++++++++++ test/test_path_manipulation.py | 119 ++++++++++++++++++ test/test_shapefile_data_extraction.py | 64 +++++++++- 17 files changed, 410 insertions(+), 36 deletions(-) create mode 100644 patchwork/path_manipulation.py create mode 100644 test/configs/config_test_mount_points.yaml create mode 100644 test/data/shapefile_mounted_unix_path/patchwork_geometries.dbf create mode 100644 test/data/shapefile_mounted_unix_path/patchwork_geometries.shp create mode 100644 test/data/shapefile_mounted_unix_path/patchwork_geometries.shx create mode 100644 test/data/shapefile_mounted_windows_path/patchwork_geometries.dbf create mode 100644 test/data/shapefile_mounted_windows_path/patchwork_geometries.shp create mode 100644 test/data/shapefile_mounted_windows_path/patchwork_geometries.shx create mode 100644 test/test_path_manipulation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e6bf0bc..6cd660b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # CHANGELOG - +- Possibilité d'utiliser des points de montage pour rediriger les chemins donnés dans le shapefile vers un autre dossier +- [Breaking change] Utilisation d'un shapefile pour définir les fichiers donneurs à utiliser pour chaque zone - génération de la carte d'indice même quand il n'y a pas de points à ajouter ## 1.1.1 diff --git a/README.md b/README.md index dd9fde1..2537191 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # patchwork -Patchwork est un outil permettant d'enrichir un fichier lidar à haute densité avec des points d'un fichier à basse densité dans les secteurs où le premier fichier n'a pas de point mais où le second en possède. +Patchwork est un outil permettant d'enrichir un fichier lidar à haute densité avec des points d'un ou plusieurs fichiers à basse densité dans les secteurs où le premier fichier n'a pas de point mais où le second en possède. ## Fonctionnement Les données en entrée sont: - un fichier lidar que l'ont souhaite enrichir -- un fichier lidar contenant des points supplémentaires +- un fichier shapefile, décrivant les fichiers qui serviront à enrichir le fichier lidar et les zones d'application potentielles (détails dans [Définition du fichier shapefile](#définition-du-fichier-shapefile)) En sortie il y a : -- Un fichier, copie du premier en entrée, enrichi des points voulus +- Un fichier, copie du premier en entrée, enrichi des points des fichiers basse densité dans les zones identifiées. -Les deux fichiers d'entrée sont découpés en tuiles carrées, généralement d'1m². Si une tuile du fichier à enrichir ne contient aucun point ayant le classement qui nous intéresse, on prend les points de la tuile de même emplacement du fichier de points supplémentaire. +Les deux fichiers d'entrée sont découpés en mailles carrées, part défaut d'1m². Si une tuile du fichier à enrichir ne contient aucun point ayant le classement qui nous intéresse, on prend les points de la tuile de même emplacement du fichier de points supplémentaire. -L'appartenance à une tuile est décidée par un arrondi par défaut, c'est-à-dire que tous les éléments de [n, n+1[ (ouvert en n+1) font parti de la même tuile. +L'appartenance à une tuile est décidée par un arrondi par défaut, c'est-à-dire que tous les éléments de [n, n+1[ (ouvert en n+1) font partie de la même tuile. ## Installation pré-requis: installer anaconda @@ -24,24 +24,23 @@ git clone https://github.com/IGNF/patchwork.git conda env create -f environment.yml conda activate patchwork ``` -## utilisation +## Utilisation Le script d'ajout de points peut être lancé via : +```bash +python main.py \ + filepath.RECIPIENT_DIRECTORY=[dossier parent du fichier receveur] \ + filepath.RECIPIENT_NAME=[nom du fichier receveur] \ + filepath.SHP_DIRECTORY=[dossier parent du shapefile] \ + filepath.SHP_NAME=[nom du fichier shapefile] \ + filepath.OUTPUT_DIR=[dossier de sortie] \ + filepath.OUTPUT_NAME=[nom du fichier de sortie] \ + [autres options] ``` -python main.py filepath.DONOR_FILE=[chemin fichier donneur] filepath.RECIPIENT_FILE=[chemin fichier receveur] filepath.OUTPUT_FILE=[chemin fichier de sortie] [autres options] -``` -Les différentes options, modifiables soit dans le fichier `configs/configs_patchwork.yaml`, soit en ligne de commande comme indiqué juste au-dessus : - -filepath.DONOR_DIRECTORY : Le répertoire du fichier qui peut donner des points à ajouter -filepath.DONOR_NAME : Le nom du fichier qui peut donner des points à ajouter -filepath.RECIPIENT_DIRECTORY : Le répertoire du fichier qui va obtenir des points en plus -filepath.RECIPIENT_NAME : Le nom du fichier qui va obtenir des points en plus -filepath.OUTPUT_DIR : Le répertoire du fichier en sortie -filepath.OUTPUT_NAME : Le nom du fichier en sortie -filepath.OUTPUT_INDICES_MAP_DIR : Le répertoire de sortie du fichier d'indice -filepath.OUTPUT_INDICES_MAP_NAME : Le nom de sortie du fichier d'indice - -DONOR_CLASS_LIST : Défaut [2, 22]. La liste des classes des points du fichier donneur qui peuvent être ajoutés. -RECIPIENT_CLASS_LIST : Défaut [2, 3, 9, 17]. La liste des classes des points du fichier receveur qui, s'ils sont absents dans une cellule, justifirons de prendre les points du fichier donneur de la même cellule -TILE_SIZE : Défaut 1000. Taille du côté de l'emprise carrée représentée par les fichiers lidar d'entrée -PATCH_SIZE : Défaut 1. taille en mètre du côté d'une cellule (doit être un diviseur de TILE_SIZE, soit pour 1000 : 0.25, 0.5, 2, 4, 5, 10, 25...) +Les différentes options sont modifiables soit dans le fichier `configs/configs_patchwork.yaml`, soit en ligne de commande comme indiqué juste au-dessus. +Voir le fichier [config_patchwork.yaml](configs/configs_patchwork.yaml) pour le détail des options + + +## Définition du fichier shapefile + +TODO \ No newline at end of file diff --git a/configs/configs_patchwork.yaml b/configs/configs_patchwork.yaml index 7c4a359..688f7b0 100644 --- a/configs/configs_patchwork.yaml +++ b/configs/configs_patchwork.yaml @@ -40,6 +40,11 @@ filepath: # path to this subdirectory can be configured using "DONOR_SUBDIRECTORY" DONOR_SUBDIRECTORY: "data" +mount_points: + - ORIGINAL_PATH: \\store\my-store # WARNING: do NOT use quotes around the path if it contains \\ + MOUNTED_PATH: /my_mounted_store/ + ORIGINAL_PLATFORM_IS_WINDOWS: true + CRS: 2154 DONOR_CLASS_LIST: [2, 22] diff --git a/patchwork/patchwork.py b/patchwork/patchwork.py index a645469..bc850d4 100644 --- a/patchwork/patchwork.py +++ b/patchwork/patchwork.py @@ -211,10 +211,7 @@ def patchwork(config: DictConfig): shapefile_path = os.path.join(config.filepath.SHP_DIRECTORY, config.filepath.SHP_NAME) donor_info_df = get_donor_info_from_shapefile( - shapefile_path, - x_shapefile, - y_shapefile, - config.filepath.DONOR_SUBDIRECTORY, + shapefile_path, x_shapefile, y_shapefile, config.filepath.DONOR_SUBDIRECTORY, config.mount_points ) complementary_bd_points = get_complementary_points( diff --git a/patchwork/path_manipulation.py b/patchwork/path_manipulation.py new file mode 100644 index 0000000..0bb7d70 --- /dev/null +++ b/patchwork/path_manipulation.py @@ -0,0 +1,38 @@ +from pathlib import Path, PurePosixPath, PureWindowsPath +from typing import Dict, List + + +def get_mounted_path_from_raw_path(raw_path: str, mount_points: List[Dict]): + """Get mounted path from a raw path and a list of mount points. + In case the raw path does not correspond to any mount point, the input raw_path is returned. + + Each mount point is described in a dictionary with keys: + - ORIGINAL_PATH (str): Original path of the mounted directory (root of the raw path to replace) + - MOUNTED_PATH (str): Mounted path of the directory (root path by which to replace the root of the raw path + in order to access to the directory on the current computer) + - ORIGINAL_PLATFORM_IS_WINDOWS (bool): true if the raw path should be interpreted as a windows path + when using this mount point + + Args: + raw_path (str): Original path to convert to a mounted path + mount_points (List[Dict]): List of mount points (as described above) + """ + mounted_path = None + for mount_point in mount_points: + mounted_path = get_mounted_path_from_mount_point(raw_path, mount_point) + if mounted_path is not None: + break + if mounted_path is None: + mounted_path = Path(raw_path) + + return mounted_path + + +def get_mounted_path_from_mount_point(raw_path, mount_point): + out_path = None + PureInputPath = PureWindowsPath if mount_point["ORIGINAL_PLATFORM_IS_WINDOWS"] else PurePosixPath + if PureInputPath(raw_path).is_relative_to(PureInputPath(mount_point["ORIGINAL_PATH"])): + relative_path = PureInputPath(raw_path).relative_to(PureInputPath(mount_point["ORIGINAL_PATH"])) + out_path = mount_point["MOUNTED_PATH"] / Path(relative_path) + + return out_path diff --git a/patchwork/shapefile_data_extraction.py b/patchwork/shapefile_data_extraction.py index 02bba63..82de460 100644 --- a/patchwork/shapefile_data_extraction.py +++ b/patchwork/shapefile_data_extraction.py @@ -1,10 +1,16 @@ import fnmatch import os +from typing import Dict, List import geopandas as gpd +from omegaconf import DictConfig +from patchwork.path_manipulation import get_mounted_path_from_raw_path -def get_donor_info_from_shapefile(input_shapefile: str, x: int, y: int, tile_subdirectory: str) -> gpd.GeoDataFrame: + +def get_donor_info_from_shapefile( + input_shapefile: str, x: int, y: int, tile_subdirectory: str, mount_points: List[Dict] | DictConfig +) -> gpd.GeoDataFrame: """Retrieve paths to all the donor files associated with a given tile (with origin x, y) from a shapefile. The shapefile should contain one geometry per donor file, with attributes: @@ -20,6 +26,13 @@ def get_donor_info_from_shapefile(input_shapefile: str, x: int, y: int, tile_sub It is stored in the "full_path" column of the output geodataframe + The mount_point dictionaries should contains these keys: + - ORIGINAL_PATH (str): Original path of the mounted directory (root of the raw path to replace) + - MOUNTED_PATH (str): Mounted path of the directory (root path by which to replace the root of the raw path + in order to access to the directory on the current computer) + - ORIGINAL_PLATFORM_IS_WINDOWS (bool): true if the raw path should be interpreted as a windows path + when using this mount point + Args: input_shapefile (str): Shapefile describing donor files x (int): x coordinate of the tile for which to get the donors @@ -27,6 +40,9 @@ def get_donor_info_from_shapefile(input_shapefile: str, x: int, y: int, tile_sub y (int): y coordinate of the tile for which to get the donors (in the same unit as in the shapefile, usually km) tile_subdirectory (str): subdirectory of "nuage_mixa" in which the donor files are stored + mount_points (List[Dict]): dictionaries describing the mount points to use to interpret paths from "nuage_mixa" + in case the path is related to a distant folder that can be mounted in different ways *(cf. dictionary + structure above) Raises: NotImplementedError: if nom_coord is false (case not handled) @@ -51,8 +67,9 @@ def get_donor_info_from_shapefile(input_shapefile: str, x: int, y: int, tile_sub if len(gdf.index): - def find_las_path_from_geometry_attributes(x: int, y: int, path_root: str): - tile_directory = os.path.join(path_root, tile_subdirectory) + def find_las_path_from_geometry_attributes(x: int, y: int, path_root: str, mount_points: List[Dict]): + mounted_path_root = get_mounted_path_from_raw_path(path_root, mount_points) + tile_directory = os.path.join(mounted_path_root, tile_subdirectory) if not os.path.isdir(tile_directory): raise FileNotFoundError(f"Directory {tile_directory} not found") potential_filenames = fnmatch.filter(os.listdir(tile_directory), f"*{x}_{y}*.la[sz]") @@ -68,10 +85,9 @@ def find_las_path_from_geometry_attributes(x: int, y: int, path_root: str): return os.path.join(tile_directory, potential_filenames[0]) gdf["full_path"] = gdf.apply( - lambda row: find_las_path_from_geometry_attributes(row["x"], row["y"], row["nuage_mixa"]), + lambda row: find_las_path_from_geometry_attributes(row["x"], row["y"], row["nuage_mixa"], mount_points), axis="columns", ) - else: gdf = gpd.GeoDataFrame(columns=["x", "y", "full_path", "geometry"]) diff --git a/test/configs/config_test_mount_points.yaml b/test/configs/config_test_mount_points.yaml new file mode 100644 index 0000000..a2581e2 --- /dev/null +++ b/test/configs/config_test_mount_points.yaml @@ -0,0 +1,63 @@ +# @package _global_ + +# path to original working directory +# hydra hijacks working directory by changing it to the current log directory, +# so it's useful to have this path as a special variable +# learn more here: https://hydra.cc/docs/next/tutorials/basic/running_your_app/working_directory +work_dir: ${hydra:runtime.cwd} + +# disable ouput directory from being created +hydra: + output_subdir: null + run: + dir: . + +# disable main.log from being created +defaults: + - override hydra/hydra_logging: disabled + - override hydra/job_logging: disabled + - _self_ + +filepath: + SHP_NAME: null # name of the shapefile used to match tiles to patch + SHP_DIRECTORY: null # path to the directory containing the shapefile + + OUTPUT_DIR: null # directory of the file with added points, from patchwork. + OUTPUT_NAME: null # name of the file with added points, from patchwork. + + INPUT_INDICES_MAP_DIR: null + INPUT_INDICES_MAP_NAME: null + + OUTPUT_INDICES_MAP_DIR: null # path to the directory for the indices map reflecting the changes to the recipient, from patchwork + OUTPUT_INDICES_MAP_NAME: null # name of the indices map reflecting the changes to the recipient, from patchwork + + RECIPIENT_DIRECTORY: null # directory containing the recipient file for patchwork + RECIPIENT_NAME: null # name of the recipient file for patchwork + + # The input shapefile should contain a "nuage_mixa" attrubute for each geometry + # "nuage_mixa" contains the path to the folder containing the files related to a specific donor source. + # Laz/las files from this source are usually contained in a subdirectory of "nuage_mixa" + # path to this subdirectory can be configured using "DONOR_SUBDIRECTORY" + DONOR_SUBDIRECTORY: "data" + +mount_points: + - ORIGINAL_PATH: \\store\my-store # WARNING: do NOT use quotes around the path if it contains \\ + MOUNTED_PATH: . + ORIGINAL_PLATFORM_IS_WINDOWS: true + - ORIGINAL_PATH: /store/my-store # WARNING: do NOT use quotes around the path if it contains \\ + MOUNTED_PATH: . + ORIGINAL_PLATFORM_IS_WINDOWS: false + +CRS: 2154 + +DONOR_CLASS_LIST: [2, 22] +RECIPIENT_CLASS_LIST: [2, 6, 9, 17] + +TILE_SIZE: 1000 +SHP_X_Y_TO_METER_FACTOR: 1000 # multiplication factor to convert shapefile x, y attributes values to meters +PATCH_SIZE: 1 # size of a patch of the grid. Must be a divisor of TILE_SIZE, so for 1000: 0.25, 0.5, 2, 4, 5, 10, 25... +NEW_COLUMN: null # If not null, contains the name of the new column +NEW_COLUMN_SIZE: 8 # must be 8, 16, 32 or 64 +VALUE_ADDED_POINTS: 1 # in case of a new column, value of the new point (the other are set to 0) +VIRTUAL_CLASS_TRANSLATION: {2: 69, 22: 70} # if there is no new column, translate the class of DONOR_CLASS_LIST into those values +# each value of DONOR_CLASS_LIST must be a key in VIRTUAL_CLASS_TRANSLATION. Not used if NEW_COLUMN is not None (or "") diff --git a/test/data/shapefile_mounted_unix_path/patchwork_geometries.dbf b/test/data/shapefile_mounted_unix_path/patchwork_geometries.dbf new file mode 100644 index 0000000000000000000000000000000000000000..f9a559fac3eda791c248587e495e54396d2ee7b3 GIT binary patch literal 2950 zcmeHIO^?$s5Di^~5CSf60qwy@kQy~jO5vK_&6bFMESm_#DKd#!EG2RAN7WwqKkQH7 z-2a8Joou=?SwvUjQq_mdWS(a{89%)oe81PZYgyKv4(v$t$HG|?nkw&M^IAE_Btv!R zSXr}3206;mXTdG&MB(?<pzuPSq3b`nmaE)?Ah|)epz%Wk$0g%b z6sY7?{!;@_W&i0{s2b4u4-A|lCbM-=<3A02PDzRw6RggEY~ZJa7LX^UfK~lx240dP zNeQ8H%)0)8fv-02dj4jA%MISzzdZN?B?VsA=l|O5*WM%0tiC&VH*=l7=eYf$wgdN7 zIGQ#aY9|V(aWFdf?W5?__RoUQjy{G_boO=@&(6bOG)~5o$bLV3<6xcPe+Ya>dy{Dl za-2a57!l0o6k@)<1TX$p@nsjJq~vJH5ukX1*FPxg0wyTP5l}bBF`~dovw%62(_(IlQ3+5!Dxip(h78eC<^kJ;n(|y<%lo z4o8glB=KPnR&6Ixww>t79GzU9vU_$5evnE%U|0&vHcIs$M5_6OHb)VR6Ap6?{*!`jMX;S0 YFFm(C!P>Z`W49pKU>gP7Wr98b4PJ*hOaK4? literal 0 HcmV?d00001 diff --git a/test/data/shapefile_mounted_unix_path/patchwork_geometries.shp b/test/data/shapefile_mounted_unix_path/patchwork_geometries.shp new file mode 100644 index 0000000000000000000000000000000000000000..b2f95df37022f8d088be7d1abbd579e371064763 GIT binary patch literal 11476 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 literal 0 HcmV?d00001 diff --git a/test/data/shapefile_mounted_unix_path/patchwork_geometries.shx b/test/data/shapefile_mounted_unix_path/patchwork_geometries.shx new file mode 100644 index 0000000000000000000000000000000000000000..59d87a4014aa29693ad81d71cbc05ad64d996e50 GIT binary patch literal 132 zcmZQzQ0HR64xC;vGcd41Ci2*ECJ0qx);NR5;xrEtx*tVHz3SRxQoq{t*@SW4nzJ5_t&|6zXu z=l)*^+sV?&%cgY_m#RMGy}Zxw`Q`c3m%U$)Iu8xQc+i0zVXiHlHld01o;I(Qb3{^9 zhmNGxi=>dF6n!hXVH`{Rv3y)v690}18EM6HQqCznmuIN@&#&Z4Hz!DJP%S9@K*Mpt zI4yHj@GAd>h9{!`Lc{+@;}*Y9fndVh;G-rB!7_#DMKUexFRUGG=kGtjJ_-TyeXt)64qy@9fQ z`>j8mG#hFs@F$@+Jaf&%;KX!KJ>Lwz_(5>`VH!@)d~Z04N8`Z!GFy1?-+`{V>Oz?)TGPU`~sIwS<~d z(-{{(gxDR0W7FwbmSuXbP_VDMf;GYq5}^kS3t`zrss4jVHJ{MtNP=;~VWz-4DcEKN a+jj9%bK4QDjaw>q4}$eKQm`#1*sFh3Up&A7 literal 0 HcmV?d00001 diff --git a/test/data/shapefile_mounted_windows_path/patchwork_geometries.shp b/test/data/shapefile_mounted_windows_path/patchwork_geometries.shp new file mode 100644 index 0000000000000000000000000000000000000000..b2f95df37022f8d088be7d1abbd579e371064763 GIT binary patch literal 11476 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 literal 0 HcmV?d00001 diff --git a/test/data/shapefile_mounted_windows_path/patchwork_geometries.shx b/test/data/shapefile_mounted_windows_path/patchwork_geometries.shx new file mode 100644 index 0000000000000000000000000000000000000000..59d87a4014aa29693ad81d71cbc05ad64d996e50 GIT binary patch literal 132 zcmZQzQ0HR64xC;vGcd41