ign-pdal-tools 1.7.6__py3-none-any.whl → 1.7.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: ign-pdal-tools
3
- Version: 1.7.6
3
+ Version: 1.7.8
4
4
  Summary: Library for common LAS files manipulation with PDAL
5
5
  Author-email: Guillaume Liegard <guillaume.liegard@ign.fr>
6
6
  Description-Content-Type: text/markdown
@@ -1,18 +1,18 @@
1
- pdaltools/_version.py,sha256=J51Tb9gFrrvFgU9fo3qbvKaTCWayFmNsRBrzb6HjUIM,74
1
+ pdaltools/_version.py,sha256=_9dnWGQSoerUjXo6a6mDsI_oUJbf9SP5h2HkjA-Zd9E,74
2
2
  pdaltools/add_points_in_las.py,sha256=TGbt5JUkszjmbQiA2LCUntsjz6A8DHb7QPIXGDuEgWA,3643
3
- pdaltools/add_points_in_pointcloud.py,sha256=W_b1eBjPPyUO20aHj8BkFgL0qNnkBs7qNnp1fGJ68JA,3725
3
+ pdaltools/add_points_in_pointcloud.py,sha256=g9V_gUA-vOy3wuYdWXFSObDZtSKO2LhzXa5Vy-yypuA,5684
4
4
  pdaltools/color.py,sha256=PSdtMMdsapOtgzojdnaKVx6IxbKOaN2xP9mScAbCGm0,8629
5
5
  pdaltools/las_add_buffer.py,sha256=sBpTywlfsHHS8KuCUa-eydB2hylshEvjrMQt5TrqXb8,11275
6
6
  pdaltools/las_clip.py,sha256=GvEOYu8RXV68e35kU8i42GwSkbo4P9TvmS6rkrdPmFM,1034
7
- pdaltools/las_info.py,sha256=cVVsOrtwgyOpI093PuXNhs2N7BJIdfW0BgxxRb-XqyQ,8096
7
+ pdaltools/las_info.py,sha256=lMKxKzsViptDENI1wOlANG4qOvdc19ixyasYKD-N1ck,9512
8
8
  pdaltools/las_merge.py,sha256=tcFVueV9X9nNEaoAl5zCduY5DETlBg63MAgP2SuKiNo,4121
9
9
  pdaltools/las_remove_dimensions.py,sha256=u_3VfkabkN_Y3eDLdJwCLVGpondvIx0f0v0RdFDoAFw,1792
10
10
  pdaltools/pcd_info.py,sha256=NIAH5KGikVDQLlbCcw9FuaPqe20UZvRfkHsDZd5kmZA,3210
11
11
  pdaltools/replace_attribute_in_las.py,sha256=po1F-fi8s7iilqKWaryW4JRbsmdMOUe0yGvG3AEKxtk,4771
12
12
  pdaltools/standardize_format.py,sha256=gqm2GJbtDkT4k4oC_NX2LIPh9R2BLh4sMHLKYgfKrMc,3973
13
13
  pdaltools/unlock_file.py,sha256=pIThdWMNkTph0xgJVVRaM1o9aUMQhM6804PscScB3JI,1963
14
- ign_pdal_tools-1.7.6.dist-info/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
15
- ign_pdal_tools-1.7.6.dist-info/METADATA,sha256=CwwZbRIegxl51Mi-0WP2_UEmARo7IBVdJf1eRxxT2bM,5722
16
- ign_pdal_tools-1.7.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
17
- ign_pdal_tools-1.7.6.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
18
- ign_pdal_tools-1.7.6.dist-info/RECORD,,
14
+ ign_pdal_tools-1.7.8.dist-info/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
15
+ ign_pdal_tools-1.7.8.dist-info/METADATA,sha256=BW25glZH1wZ1Ntgp6MQbPa_iMJZrJNBTs_acW7AxVYg,5722
16
+ ign_pdal_tools-1.7.8.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
17
+ ign_pdal_tools-1.7.8.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
18
+ ign_pdal_tools-1.7.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pdaltools/_version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.7.6"
1
+ __version__ = "1.7.8"
2
2
 
3
3
 
4
4
  if __name__ == "__main__":
@@ -1,47 +1,63 @@
1
+ import argparse
2
+ import shutil
3
+
1
4
  import geopandas as gpd
2
5
  import laspy
3
6
  import numpy as np
7
+ from pyproj import CRS
8
+ from pyproj.exceptions import CRSError
4
9
  from shapely.geometry import box
5
10
 
6
- from pdaltools.las_info import get_tile_origin_using_header_info
7
-
8
-
9
- def get_tile_bbox(input_las, tile_width=1000) -> tuple:
10
- """
11
- Get the theoretical bounding box (xmin, ymin, xmax, ymax) of a LIDAR tile
12
- using its origin and the predefined tile width.
13
-
14
- Args:
15
- input_las (str): Path to the LIDAR `.las/.laz` file.
16
- tile_width (int): Width of the tile in meters (default: 1000).
17
-
18
- Returns:
19
- tuple: Bounding box as (xmin, ymin, xmax, ymax).
20
- """
21
- origin_x, origin_y = get_tile_origin_using_header_info(input_las)
22
- bbox = (origin_x, origin_y - tile_width, origin_x + tile_width, origin_y)
23
- return bbox
24
-
25
-
26
- def clip_3d_points_to_tile(input_points: str, input_las: str, crs: str) -> gpd.GeoDataFrame:
11
+ from pdaltools.las_info import get_epsg_from_las, get_tile_bbox
12
+
13
+
14
+ def parse_args(argv=None):
15
+ parser = argparse.ArgumentParser("Add points from GeoJSON in LIDAR tile")
16
+ parser.add_argument("--input_geojson", "-ig", type=str, required=True, help="Input GeoJSON file")
17
+ parser.add_argument("--input_las", "-i", type=str, required=True, help="Input las file")
18
+ parser.add_argument("--output_las", "-o", type=str, required=True, default="", help="Output las file")
19
+ parser.add_argument(
20
+ "--virtual_points_classes",
21
+ "-c",
22
+ type=int,
23
+ default=66,
24
+ help="classification value to assign to the added virtual points",
25
+ )
26
+ parser.add_argument(
27
+ "--spatial_ref",
28
+ type=str,
29
+ required=False,
30
+ help="spatial reference for the writer",
31
+ )
32
+ parser.add_argument(
33
+ "--tile_width",
34
+ type=int,
35
+ default=1000,
36
+ help="width of tiles in meters",
37
+ )
38
+
39
+ return parser.parse_args(argv)
40
+
41
+
42
+ def clip_3d_points_to_tile(input_points: str, input_las: str, crs: str, tile_width: int) -> gpd.GeoDataFrame:
27
43
  """
28
44
  Add points from a GeoJSON file in the LIDAR's tile.
29
45
 
30
46
  Args:
31
47
  input_points (str): Path to the input GeoJSON file with 3D points.
32
48
  input_las (str): Path to the LIDAR `.las/.laz` file.
33
- crs (str): CRS of the data, e.g., 'EPSG:2154'.
49
+ crs (str): CRS of the data.
50
+ tile_width (int): Width of the tile in meters (default: 1000).
34
51
 
35
52
  Return:
36
53
  gpd.GeoDataFrame: Points 2d with "Z" value
37
54
  """
38
55
  # Compute the bounding box of the LIDAR tile
39
- tile_bbox = get_tile_bbox(input_las)
56
+ tile_bbox = get_tile_bbox(input_las, tile_width)
40
57
 
41
58
  # Read the input GeoJSON with 3D points
42
59
  points_gdf = gpd.read_file(input_points)
43
60
 
44
- # Ensure the CRS matches
45
61
  if crs:
46
62
  points_gdf = points_gdf.to_crs(crs)
47
63
 
@@ -55,7 +71,7 @@ def clip_3d_points_to_tile(input_points: str, input_las: str, crs: str) -> gpd.G
55
71
 
56
72
 
57
73
  def add_points_to_las(
58
- input_points_with_z: gpd.GeoDataFrame, input_las: str, output_las: str, virtual_points_classes=66
74
+ input_points_with_z: gpd.GeoDataFrame, input_las: str, output_las: str, crs: str, virtual_points_classes=66
59
75
  ):
60
76
  """Add points (3D points in LAZ format) by LIDAR tiles (tiling file)
61
77
 
@@ -63,11 +79,17 @@ def add_points_to_las(
63
79
  input_points_with_z(gpd.GeoDataFrame): geometry columns (2D points) as encoded to WKT.
64
80
  input_las (str): Path to the LIDAR tiles (LAZ).
65
81
  output_las (str): Path to save the updated LIDAR file (LAS/LAZ format).
82
+ crs (str): CRS of the data.
66
83
  virtual_points_classes (int): The classification value to assign to those virtual points (default: 66).
67
84
  """
68
- # Check if input points are empty
85
+
69
86
  if input_points_with_z.empty:
70
- raise ValueError("No points to add. The input GeoDataFrame is empty.")
87
+ print(
88
+ "No points to add. All points of the geojson file are outside the tile. Copying the input file to output"
89
+ )
90
+ shutil.copy(input_las, output_las)
91
+
92
+ return
71
93
 
72
94
  # Extract XYZ coordinates and additional attribute (classification)
73
95
  x_coords = input_points_with_z.geometry.x
@@ -75,28 +97,63 @@ def add_points_to_las(
75
97
  z_coords = input_points_with_z.RecupZ
76
98
  classes = virtual_points_classes * np.ones(len(input_points_with_z.index))
77
99
 
78
- # Read the existing LIDAR file
79
100
  with laspy.open(input_las, mode="r") as las:
80
101
  las_data = las.read()
81
102
  header = las.header
82
103
 
83
- # Create a new header if the original header is missing or invalid
84
- if header is None:
85
- header = laspy.LasHeader(point_format=6, version="1.4") # Example format and version
104
+ if not header:
105
+ header = laspy.LasHeader(point_format=8, version="1.4")
106
+ if crs:
107
+ try:
108
+ crs_obj = CRS.from_user_input(crs) # Convert to a pyproj.CRS object
109
+ except CRSError:
110
+ raise ValueError(f"Invalid CRS: {crs}")
111
+ header.add_crs(crs_obj)
86
112
 
87
- # Append the clipped points to the existing LIDAR data
113
+ # Append new points
88
114
  new_x = np.concatenate([las_data.x, x_coords])
89
115
  new_y = np.concatenate([las_data.y, y_coords])
90
116
  new_z = np.concatenate([las_data.z, z_coords])
91
117
  new_classes = np.concatenate([las_data.classification, classes])
92
118
 
93
- # Create a new LAS file with updated data
94
119
  updated_las = laspy.LasData(header)
95
120
  updated_las.x = new_x
96
121
  updated_las.y = new_y
97
122
  updated_las.z = new_z
98
123
  updated_las.classification = new_classes
99
124
 
100
- # Write the updated LAS file
101
125
  with laspy.open(output_las, mode="w", header=header, do_compress=True) as writer:
102
126
  writer.write_points(updated_las.points)
127
+
128
+
129
+ def add_points_from_geojson_to_las(
130
+ input_geojson: str, input_las: str, output_las: str, virtual_points_classes: int, spatial_ref: str, tile_width: int
131
+ ):
132
+ """Add points with Z value(GeoJSON format) by LIDAR tiles (tiling file)
133
+
134
+ Args:
135
+ input_geojson (str): Path to the input GeoJSON file with 3D points.
136
+ input_las (str): Path to the LIDAR `.las/.laz` file.
137
+ output_las (str): Path to save the updated LIDAR file (LAS/LAZ format).
138
+ virtual_points_classes (int): The classification value to assign to those virtual points (default: 66).
139
+ spatial_ref (str): CRS of the data.
140
+ tile_width (int): Width of the tile in meters (default: 1000).
141
+
142
+ Raises:
143
+ RuntimeError: If the input LAS file has no valid EPSG code.
144
+ """
145
+ if not spatial_ref:
146
+ spatial_ref = get_epsg_from_las(input_las)
147
+ if spatial_ref is None:
148
+ raise RuntimeError(f"LAS file {input_las} does not have a valid EPSG code.")
149
+
150
+ # Clip points from GeoJSON by LIDAR tile
151
+ points_clipped = clip_3d_points_to_tile(input_geojson, input_las, spatial_ref, tile_width)
152
+
153
+ # Add points by LIDAR tile and save the result
154
+ add_points_to_las(points_clipped, input_las, output_las, spatial_ref, virtual_points_classes)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ args = parse_args()
159
+ add_points_from_geojson_to_las(**vars(args))
pdaltools/las_info.py CHANGED
@@ -1,8 +1,10 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
+ from pathlib import Path
4
5
  from typing import Dict, Tuple
5
6
 
7
+ import laspy
6
8
  import osgeo.osr as osr
7
9
  import pdal
8
10
 
@@ -50,6 +52,23 @@ def get_tile_origin_using_header_info(filename: str, tile_width: int = 1000) ->
50
52
  return infer_tile_origin(minx, maxx, miny, maxy, tile_width)
51
53
 
52
54
 
55
+ def get_tile_bbox(input_las, tile_width=1000) -> tuple:
56
+ """
57
+ Get the theoretical bounding box (xmin, ymin, xmax, ymax) of a LIDAR tile
58
+ using its origin and the predefined tile width.
59
+
60
+ Args:
61
+ input_las (str): Path to the LIDAR `.las/.laz` file.
62
+ tile_width (int): Width of the tile in meters (default: 1000).
63
+
64
+ Returns:
65
+ tuple: Bounding box as (xmin, ymin, xmax, ymax).
66
+ """
67
+ origin_x, origin_y = get_tile_origin_using_header_info(input_las, tile_width)
68
+ bbox = (origin_x, origin_y - tile_width, origin_x + tile_width, origin_y)
69
+ return bbox
70
+
71
+
53
72
  def get_epsg_from_header_info(metadata):
54
73
  if "srs" not in metadata.keys():
55
74
  raise RuntimeError("EPSG could not be inferred from metadata: No 'srs' key in metadata.")
@@ -142,7 +161,14 @@ def parse_filename(file: str):
142
161
  For example Semis_2021_0000_1111_LA93_IGN69.las"""
143
162
  basename = os.path.basename(file) # Make sure that we work on the base name and not the full path
144
163
 
145
- prefix1, prefix2, coordx, coordy, suffix = basename.split("_", 4)
164
+ try:
165
+ prefix1, prefix2, coordx, coordy, suffix = basename.split("_", 4)
166
+ except ValueError:
167
+ raise ValueError(
168
+ f"Filename {Path(file).name} does not have the expected format. "
169
+ "Expected prefix1_prefix2_coordx_coordy_suffix"
170
+ )
171
+
146
172
  prefix = f"{prefix1}_{prefix2}"
147
173
 
148
174
  return prefix, int(coordx), int(coordy), suffix
@@ -211,3 +237,20 @@ def get_writer_parameters_from_reader_metadata(metadata: Dict, a_srs=None) -> Di
211
237
  "a_srs": a_srs if a_srs else reader_metadata["comp_spatialreference"],
212
238
  }
213
239
  return params
240
+
241
+
242
+ def get_epsg_from_las(filename: str) -> str:
243
+ """Extract EPSG code from LAS file metadata and return as 'EPSG:XXXX' format.
244
+
245
+ Args:
246
+ filename (str): full path of file for which to get the bounding box
247
+
248
+ Returns:
249
+ str : CRS's value of the data in 'EPSG:XXXX' format, or None if not found.
250
+ """
251
+ with laspy.open(filename) as las:
252
+ crs = las.header.parse_crs()
253
+ if crs is None:
254
+ return None # Return None if CRS is not defined
255
+ epsg_code = crs.to_epsg()
256
+ return f"EPSG:{epsg_code}" if epsg_code else None