-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from IGNF/virtual_point
Z on Virtual points
- Loading branch information
Showing
28 changed files
with
901 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
**/__pycache__ | ||
tmp | ||
main.log | ||
**.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# -*- coding: utf-8 -*- | ||
""" Filter pointcloud to keep only the ground class """ | ||
from typing import List | ||
|
||
import numpy as np | ||
import pdal | ||
from shapely.geometry import MultiPolygon | ||
|
||
|
||
def read_filter_and_crop_pointcloud(input_points: str, geom: MultiPolygon, classes: List[int]) -> np.array: | ||
"""Filter pointcloud to keep only the ground class | ||
Args: | ||
input_points (str): Path to the input LAS/LAZ file | ||
geom (MultiPolygon): An array of WKT or GeoJSON 2D MultiPolygon (Mask Hydro with buffer) | ||
classes (List[int]): Classification values to keep for input points | ||
Returns: | ||
np.array : Numpy array containing point coordinates (X, Y, Z) after filtering and croping | ||
""" | ||
# Crop pointcloud by point | ||
pipeline = ( | ||
pdal.Reader.las(filename=input_points, nosrs=True) | ||
| pdal.Filter.range( | ||
limits=",".join(f"Classification[{v}:{v}]" for v in classes), | ||
) | ||
| pdal.Filter.crop( | ||
polygon=geom, | ||
) | ||
) | ||
pipeline.execute() | ||
# extract points | ||
cropped_points = pipeline.arrays[0] | ||
|
||
return np.array([cropped_points["X"], cropped_points["Y"], cropped_points["Z"]]).T |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# -*- coding: utf-8 -*- | ||
""" Calculates statistics from np.array (ex. pointcloud) """ | ||
import numpy as np | ||
|
||
|
||
def calculate_percentile(points: np.array, q: int) -> float: | ||
""" | ||
Calculates the quartile's value of Z coordinates | ||
Parameters: | ||
- points (numpy.ndarray): An array of 3D points [x, y, z] | ||
- q (int): Percentage for the percentile to compute. | ||
Values must be between 0 and 100 inclusive. | ||
Returns: | ||
- float: The quartile of Z coordinates | ||
""" | ||
altitudes = points[:, 2] # Extract the altitude column | ||
n_quartile = np.round(np.percentile(altitudes, q), 2) | ||
|
||
return n_quartile | ||
|
||
|
||
def calculate_median(points: np.array) -> float: | ||
""" | ||
Calculates the median's value of Z coordinates | ||
Parameters: | ||
- points (numpy.ndarray): An array of 3D points [x, y, z] | ||
Returns: | ||
- float: The median of Z coordinates | ||
""" | ||
altitudes = points[:, 2] # Extract the altitude column | ||
n_median = np.round(np.median(altitudes), 2) | ||
|
||
return n_median |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# -*- coding: utf-8 -*- | ||
""" KNN """ | ||
import numpy as np | ||
from sklearn.neighbors import NearestNeighbors | ||
|
||
|
||
def find_k_nearest_neighbors(points_skeleton: np.array, points_ground_3d: np.array, k: int) -> np.array: | ||
"""Finds the K nearest neighbors of a given point from a list of 3D points | ||
Args: | ||
points_skeleton (np.array): a single point representing the skeleton Hydro | ||
an array of 2D point [x, y] | ||
points_ground_3d (np.array): ground's points from LIDAR. | ||
an array of 3D points [x, y, z] | ||
k (int): the number of nearest neighbors to find | ||
Returns: | ||
numpy.ndarray: an array of the K nearest 3D points [x, y, z] | ||
""" | ||
# Extract 2D coordinates (x, y) from the 3D points | ||
points_ground_2d = points_ground_3d[:, :2] | ||
|
||
# Convert point_2d to nump.array | ||
points_skeleton_array = np.array(points_skeleton).reshape(1, -1) | ||
|
||
# Initialize and fit the NearestNeighbors model | ||
nbrs = NearestNeighbors(n_neighbors=k, algorithm="auto").fit(points_ground_2d) | ||
|
||
# Find the distances and indices of the nearest neighbors | ||
indices = nbrs.kneighbors(points_skeleton_array, return_distance=False) | ||
|
||
# Retrieve the points corresponding to the indices | ||
# Use indices to retrieve the closest 3D points | ||
k_nearest_points = points_ground_3d[indices[0]] | ||
# = points_3d[indices.flatten()] | ||
|
||
return k_nearest_points |
30 changes: 30 additions & 0 deletions
30
lidro/create_virtual_point/vectors/clip_points_with_bounding_box.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# -*- coding: utf-8 -*- | ||
""" Clip Skeleton points by tile (pointcloud) | ||
""" | ||
import geopandas as gpd | ||
from shapely.geometry import box | ||
|
||
|
||
def clip_points_with_box(gdf_points: gpd.GeoDataFrame, bbox: tuple) -> gpd.GeoDataFrame: | ||
"""Clip skeleton points by tile (bounding box) | ||
Args: | ||
gdf_points (gpd.GeoDataFrame): Points every 2 meters (by default) along skeleton hydro | ||
bbox (tuple): bounding box from tile (pointcloud) | ||
Returns: | ||
gpd.GeoDataframe : Points every 2 meters (by default) along skeleton hydro by tile | ||
""" | ||
# Extract the bounding box limits | ||
xmin = bbox[0][0] | ||
xmax = bbox[0][1] | ||
ymin = bbox[1][0] | ||
ymax = bbox[1][1] | ||
|
||
# Create a polygon representing the bounding box | ||
bounding_box = box(xmin, ymin, xmax, ymax) | ||
|
||
# Clip points using the bounding box | ||
clipped_points = gdf_points[gdf_points.within(bounding_box)] | ||
|
||
return clipped_points |
63 changes: 63 additions & 0 deletions
63
lidro/create_virtual_point/vectors/extract_points_around_skeleton.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# -*- coding: utf-8 -*- | ||
""" Extract points around skeleton by tile | ||
""" | ||
import logging | ||
import os | ||
from typing import List | ||
|
||
import geopandas as gpd | ||
from pdaltools.las_info import las_get_xy_bounds | ||
from shapely.geometry import Point | ||
|
||
from lidro.create_virtual_point.pointcloud.crop_las import ( | ||
read_filter_and_crop_pointcloud, | ||
) | ||
from lidro.create_virtual_point.vectors.clip_points_with_bounding_box import ( | ||
clip_points_with_box, | ||
) | ||
from lidro.create_virtual_point.vectors.las_around_point import filter_las_around_point | ||
|
||
|
||
def extract_points_around_skeleton_points_one_tile( | ||
filename: str, | ||
input_dir: str, | ||
input_mask_hydro_buffer: gpd.GeoDataFrame, | ||
points_skeleton_gdf: gpd.GeoDataFrame, | ||
classes: List[int:int], | ||
k: int, | ||
): | ||
"""Severals steps : | ||
1- Crop filtered pointcloud by Mask Hydro with buffer | ||
2- Extract a Z elevation value along the hydrographic skeleton | ||
Args: | ||
filename (str): filename to the LAS file | ||
input_dir (str): input folder | ||
input_mask_hydro_buffer (gpd.GeoDataFrame): hydro mask with buffer | ||
points_skeleton_gdf (gpd.GeoDataFrame): Points every 2 meters (by default) along skeleton hydro | ||
classes (List): List of classes to use for the filtering | ||
k (int): the number of nearest neighbors to find | ||
Returns: | ||
points_clip (np.array) : Numpy array containing point coordinates (X, Y, Z) after filtering and croping | ||
""" | ||
# Step 1 : Crop filtered pointcloud by Mask Hydro with buffer | ||
input_dir_points = os.path.join(input_dir, "pointcloud") | ||
tilename = os.path.splitext(filename)[0] # filename to the LAS file | ||
input_pointcloud = os.path.join(input_dir_points, filename) # path to the LAS file | ||
logging.info(f"\nCroping filtered pointcloud by Mask Hydro with buffer for tile : {tilename}") | ||
# Croping filtered pointcloud by Mask Hydro with buffer for tile | ||
points_clip = read_filter_and_crop_pointcloud(input_pointcloud, str(input_mask_hydro_buffer), classes) | ||
logging.info(f"\nCropping skeleton points for tile: {tilename}") | ||
# Extract bounding box for clipping points by tile | ||
bbox = las_get_xy_bounds(input_pointcloud) | ||
points_skeleton_clip = clip_points_with_box(points_skeleton_gdf, bbox) | ||
# Create list with Skeleton's points with Z for step 4 | ||
points_skeleton_with_z_clip = [ | ||
([geom.x, geom.y]) for geom in points_skeleton_clip.geometry if isinstance(geom, Point) | ||
] | ||
# Step 2 : Extract a Z elevation value along the hydrographic skeleton | ||
logging.info(f"\nExtract a Z elevation value along the hydrographic skeleton for tile : {tilename}") | ||
result = filter_las_around_point(points_skeleton_with_z_clip, points_clip, k) | ||
|
||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# -*- coding: utf-8 -*- | ||
""" Extract a Z elevation value every N meters along the hydrographic skeleton | ||
""" | ||
from typing import List | ||
|
||
import numpy as np | ||
from shapely.geometry import Point | ||
|
||
from lidro.create_virtual_point.stats.calculate_stat import calculate_percentile | ||
from lidro.create_virtual_point.stats.knn_distance import find_k_nearest_neighbors | ||
|
||
|
||
def filter_las_around_point(points_skeleton: List, points_clip: np.array, k: int) -> List: | ||
"""Extract a Z elevation value every N meters along the hydrographic skeleton | ||
Args: | ||
points_skeleton (list) : points every N meters (by default: 2) along skeleton Hydro | ||
points_clip (np.array): Numpy array containing point coordinates (X, Y, Z) after filtering and croping | ||
k (int): The number of nearest neighbors to find | ||
Returns: | ||
List : Result {'geometry': Point 3D, 'z_q1': points KNN} | ||
""" | ||
# Finds the K nearest neighbors of a given point from a list of 3D points | ||
points_knn_list = [ | ||
({"geometry": Point(point), "points_knn": find_k_nearest_neighbors(point, points_clip, k)}) | ||
for point in points_skeleton | ||
] | ||
|
||
# Calcule Z "Q1" for each points every N meters along skeleton hydro | ||
results = [ | ||
({"geometry": p["geometry"], "z_q1": calculate_percentile(p["points_knn"], 10)}) | ||
for p in points_knn_list | ||
if not np.all(np.isnan(p["points_knn"])) | ||
] | ||
|
||
return results |
28 changes: 28 additions & 0 deletions
28
lidro/create_virtual_point/vectors/mask_hydro_with_buffer.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# -*- coding: utf-8 -*- | ||
""" Extract a Z elevation value every N meters along the hydrographic skeleton | ||
""" | ||
import geopandas as gpd | ||
from shapely.geometry import CAP_STYLE | ||
|
||
|
||
def import_mask_hydro_with_buffer(file_mask: str, buffer: float, crs: str | int) -> gpd.GeoDataFrame: | ||
"""Apply buffer (2 meters by default) from Mask Hydro | ||
Args: | ||
file_mask (str): Path from Mask Hydro | ||
buffer (float): buffer to apply to the input geometry | ||
crs (str | int): Make a CRS from an EPSG code : CRS WKT string | ||
Returns: | ||
gpd.GeoDataFrame : geometry columns are encoded to WKT | ||
""" | ||
# Read Mask Hydro merged | ||
gdf = gpd.read_file(file_mask, crs=crs).unary_union | ||
|
||
# Apply buffer (2 meters by default) from Mask Hydro | ||
gdf_buffer = gdf.buffer(buffer, cap_style=CAP_STYLE.square) | ||
|
||
# Return a polygon representing the limit of the bank with a buffer of N meters | ||
limit_bank = gdf_buffer.difference(gdf) | ||
|
||
return limit_bank |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.