ign-pdal-tools 1.6.0__py3-none-any.whl → 1.7.0__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.1
2
2
  Name: ign-pdal-tools
3
- Version: 1.6.0
3
+ Version: 1.7.0
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,14 +1,15 @@
1
- pdaltools/_version.py,sha256=Xq8G0zFAUQ7LL7Y4z3YbruoLpmF2hoaWnL6BPpSAP6M,74
1
+ pdaltools/_version.py,sha256=9fOW18Rr_2UaEKhSgKdd6D5gPk-Z2COb77UpIg5Oq20,74
2
2
  pdaltools/color.py,sha256=7U-SThIKqrfE1xXXnFqpbIhmZEqna29nRiyLW8l8Y1c,8075
3
- pdaltools/las_add_buffer.py,sha256=_syALdaf6ks5KSINlfkALwGUrPOHLhK8rGlYVwDamzc,5653
3
+ pdaltools/las_add_buffer.py,sha256=sBpTywlfsHHS8KuCUa-eydB2hylshEvjrMQt5TrqXb8,11275
4
4
  pdaltools/las_clip.py,sha256=GvEOYu8RXV68e35kU8i42GwSkbo4P9TvmS6rkrdPmFM,1034
5
5
  pdaltools/las_info.py,sha256=RE-UBdEUXqKvSrMV3mOlvE_16mhum7bw-p-ERu5bGOc,6979
6
6
  pdaltools/las_merge.py,sha256=tcFVueV9X9nNEaoAl5zCduY5DETlBg63MAgP2SuKiNo,4121
7
+ pdaltools/las_remove_dimensions.py,sha256=0zhv9LBvlL69TLmXTJlRQcUBOaBmCRZEQU2Qadx27aM,1805
7
8
  pdaltools/replace_attribute_in_las.py,sha256=po1F-fi8s7iilqKWaryW4JRbsmdMOUe0yGvG3AEKxtk,4771
8
9
  pdaltools/standardize_format.py,sha256=KM_jC_aC9yLD5rrSUGgTwfyakbh86FXsAI-y8gokF4M,2883
9
10
  pdaltools/unlock_file.py,sha256=pIThdWMNkTph0xgJVVRaM1o9aUMQhM6804PscScB3JI,1963
10
- ign_pdal_tools-1.6.0.dist-info/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
11
- ign_pdal_tools-1.6.0.dist-info/METADATA,sha256=3BZwbcQ_mDIr1XzK5JZMrjIQmRcN__FW62GMp9FKrIQ,4825
12
- ign_pdal_tools-1.6.0.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
13
- ign_pdal_tools-1.6.0.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
14
- ign_pdal_tools-1.6.0.dist-info/RECORD,,
11
+ ign_pdal_tools-1.7.0.dist-info/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
12
+ ign_pdal_tools-1.7.0.dist-info/METADATA,sha256=-dCrIES5-avXftir_W05j3rnc5w0PO7trYeyB7pLIY4,4825
13
+ ign_pdal_tools-1.7.0.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
14
+ ign_pdal_tools-1.7.0.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
15
+ ign_pdal_tools-1.7.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.0)
2
+ Generator: setuptools (70.3.0)
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.6.0"
1
+ __version__ = "1.7.0"
2
2
 
3
3
 
4
4
  if __name__ == "__main__":
@@ -1,7 +1,10 @@
1
1
  import argparse
2
2
  import logging
3
3
  import os
4
- from typing import List
4
+ import tempfile
5
+ from functools import wraps
6
+ from pathlib import Path
7
+ from typing import Callable, List
5
8
 
6
9
  import pdal
7
10
 
@@ -10,6 +13,9 @@ from pdaltools.las_info import (
10
13
  get_writer_parameters_from_reader_metadata,
11
14
  )
12
15
  from pdaltools.las_merge import create_list
16
+ from pdaltools.las_remove_dimensions import remove_dimensions_from_las
17
+
18
+ ORIGINAL_TILE_TAG = "is_in_original"
13
19
 
14
20
 
15
21
  def create_las_with_buffer(
@@ -20,19 +26,27 @@ def create_las_with_buffer(
20
26
  spatial_ref: str = "EPSG:2154",
21
27
  tile_width: int = 1000,
22
28
  tile_coord_scale: int = 1000,
29
+ tag_original_tile: bool = False,
23
30
  ):
24
31
  """Merge lidar tiles around the queried tile and crop them in order to add a buffer
25
32
  to the tile (usually 100m).
33
+
26
34
  Args:
27
- input_dir (str): directory of pointclouds (where you look for neigbors)
28
- tile_filename (str): full path to the queried LIDAR tile
29
- output_filename (str) : full path to the saved cropped tile
30
- buffer_width (int): width of the border to add to the tile (in pixels)
31
- spatial_ref (str): Spatial reference to use to override the one from input las.
32
- tile width (int): width of tiles in meters (usually 1000m)
33
- tile_coord_scale (int) : scale used in the filename to describe coordinates in meters
34
- (usually 1000m)
35
+ input_dir (str): directory of pointclouds (where you look for neighbors)
36
+ tile_filename (str): full path to the queried LIDAR tile
37
+ output_filename (str): full path to the saved cropped tile
38
+ buffer_width (int, optional): width of the border to add to the tile (in meters).
39
+ Defaults to 100.
40
+ spatial_ref (_type_, optional): Spatial reference to use to override the one from input las.
41
+ Defaults to "EPSG:2154".
42
+ tile_width (int, optional): width of tiles in meters. Defaults to 1000.
43
+ tile_coord_scale (int, optional): scale used in the filename to describe coordinates
44
+ in meters. Defaults to 1000.
45
+ tag_original_tile (bool, optional): if true, add a new "is_in_original" dimension
46
+ to the output las, equal to 1 on points that belong to the original tile, 0 on points
47
+ that belong to the added buffer. Defaults to False.
35
48
  """
49
+
36
50
  bounds = get_buffered_bounds_from_filename(
37
51
  tile_filename, buffer_width=buffer_width, tile_width=tile_width, tile_coord_scale=tile_coord_scale
38
52
  )
@@ -46,6 +60,7 @@ def create_las_with_buffer(
46
60
  spatial_ref,
47
61
  tile_width=tile_width,
48
62
  tile_coord_scale=tile_coord_scale,
63
+ tag_original_tile=tag_original_tile,
49
64
  )
50
65
 
51
66
 
@@ -57,6 +72,7 @@ def las_merge_and_crop(
57
72
  spatial_ref: str = "EPSG:2154",
58
73
  tile_width=1000,
59
74
  tile_coord_scale=1000,
75
+ tag_original_tile: bool = False,
60
76
  ):
61
77
  """Merge and crop las in a single pipeline (for buffer addition)
62
78
 
@@ -65,29 +81,40 @@ def las_merge_and_crop(
65
81
  - For each file:
66
82
  - read it
67
83
  - crop it according to the bounds
84
+ - optionally add a dimension to differentiate points from the central pointscloud
85
+ from those added as a buffer
68
86
  - keep the crop in memory
69
87
  - delete the pipeline object to release the memory taken by the las reader
70
88
  - Merge the already cropped data
71
89
 
72
90
  Args:
73
- input_dir (str): directory of pointclouds (where you look for neigbors)
91
+ input_dir (str): directory of pointclouds (where you look for neighbors)
74
92
  tile_filename (str): full path to the queried LIDAR tile
75
- bounds : 2D bounding box to crop to : provided as ([xmin, xmax], [ymin, ymax])
76
- output_filename (str) : full path to the saved cropped tile
77
- spatial_ref (str): spatial reference for the writer
78
- tile width (int): width of tiles in meters (usually 1000m)
79
- tile_coord_scale (int) : scale used in the filename to describe coordinates in meters
80
- (usually 1000m)
93
+ bounds (List): 2D bounding box to crop to : provided as ([xmin, xmax], [ymin, ymax])
94
+ output_filename (str): full path to the saved cropped tile
95
+ spatial_ref (str, optional): spatial reference for the writer. Defaults to "EPSG:2154".
96
+ tile_width (int, optional): width of tiles in meters (usually 1000m). Defaults to 1000.
97
+ tile_coord_scale (int, optional): scale used in the filename to describe coordinates in meters.
98
+ Defaults to 1000.
99
+ tag_original_tile (bool, optional): if true, add a new "is_in_original" dimension
100
+ to the output las, equal to 1 on points that belong to the original tile, 0 on points
101
+ that belong to the added buffer. Defaults to False.
102
+ Raises:
103
+ ValueError: if the list of tiles to merge is empty
81
104
  """
105
+
82
106
  # List files to merge
83
107
  files_to_merge = create_list(input_dir, tile_filename, tile_width, tile_coord_scale)
84
-
108
+ central_file = files_to_merge[-1]
85
109
  if len(files_to_merge) > 0:
86
110
  # Read and crop each file
87
111
  crops = []
88
112
  for f in files_to_merge:
89
113
  pipeline = pdal.Pipeline()
90
114
  pipeline |= pdal.Reader.las(filename=f, override_srs=spatial_ref)
115
+ if tag_original_tile:
116
+ pipeline |= pdal.Filter.ferry(dimensions=f"=>{ORIGINAL_TILE_TAG}")
117
+ pipeline |= pdal.Filter.assign(value=f"{ORIGINAL_TILE_TAG}={int(f == central_file)}")
91
118
  pipeline |= pdal.Filter.crop(bounds=str(bounds))
92
119
  pipeline.execute()
93
120
  if len(pipeline.arrays[0]) == 0:
@@ -95,10 +122,9 @@ def las_merge_and_crop(
95
122
  else:
96
123
  crops.append(pipeline.arrays[0])
97
124
 
98
- # Retrieve metadata before the pipeline is deleted
99
- # As the last file of files_to_merge is the central one, metadata will contain the info
100
- # from the central file after the last iteration of the for loop
101
- metadata = pipeline.metadata
125
+ if f == central_file:
126
+ # Retrieve metadata before the pipeline is deleted
127
+ metadata = pipeline.metadata
102
128
  del pipeline
103
129
 
104
130
  params = get_writer_parameters_from_reader_metadata(metadata, a_srs=spatial_ref)
@@ -116,6 +142,106 @@ def las_merge_and_crop(
116
142
  pass
117
143
 
118
144
 
145
+ def remove_points_from_buffer(input_file: str, output_file: str):
146
+ """Remove the points that were added as a buffer to a las file using the "is_in_original"
147
+ dimension that has been added by create_las_with_buffer
148
+
149
+ Limitation: if any point has been added to the point cloud after adding the buffer, it
150
+ won't be preserved by this operation (only points from the original file are kept)
151
+
152
+ Args:
153
+ input_file (str): path to the input file containing the "is_in_original" dimension
154
+ output_file (str): path to the output_file
155
+ """
156
+ with tempfile.NamedTemporaryFile(suffix="_with_additional_dim.las") as tmp_las:
157
+ pipeline = pdal.Pipeline() | pdal.Reader.las(input_file)
158
+ pipeline |= pdal.Filter.range(limits=f"{ORIGINAL_TILE_TAG}[1:1]")
159
+ pipeline |= pdal.Writer.las(filename=tmp_las.name, forward="all", extra_dims="all")
160
+ pipeline.execute()
161
+
162
+ remove_dimensions_from_las(tmp_las.name, dimensions=[ORIGINAL_TILE_TAG], output_las=output_file)
163
+
164
+
165
+ def run_on_buffered_las(
166
+ buffer_width: int, spatial_ref: str, tile_width: int = 1000, tile_coord_scale: int = 1000
167
+ ) -> Callable:
168
+ """Decorator to apply a function that takes a las/laz as input and returns a las/laz output
169
+ on an input with an additional buffer, then remove the buffer points from the output
170
+
171
+ The first argument of the decorated function must be an input path
172
+ The second argument of the decorated function must be an output path
173
+
174
+ The buffer is added by merging lidar tiles around the queried tile and crop them based
175
+ on their filenames.
176
+
177
+ Limitation: if any point has been added to the point cloud by the decorated function, it
178
+ won't be preserved by this operation (only points from the original file are kept)
179
+
180
+
181
+ Args:
182
+ buffer_width (int): width of the border to add to the tile (in meters)
183
+ spatial_ref (str): spatial reference for the writer. Example: "EPSG:2154".
184
+ tile_width (int, optional): width of tiles in meters (usually 1000m). Defaults to 1000.
185
+ tile_coord_scale (int, optional): scale used in the filename to describe coordinates in meters.
186
+ Defaults to 1000.
187
+
188
+ Raises:
189
+ FileNotFoundError: when the first argument of the decorated function is not an existing
190
+ file
191
+ FileNotFoundError: when the second argument of the decorated function is not a path
192
+ with an existing parent folder
193
+
194
+ Returns:
195
+ Callable: decorated function
196
+ """
197
+ """Decorator to run a function that takes a las as input and returns a las output
198
+ on a las with an additional buffer, then remove the buffer points from the buffer points
199
+
200
+ """
201
+
202
+ def decorator(func):
203
+ @wraps(func)
204
+ def wrapper(*args, **kwargs):
205
+ input_file = args[0]
206
+ output_file = args[1]
207
+ if not Path(input_file).is_file():
208
+ raise FileNotFoundError(
209
+ f"File {args[0]} not found. The first argument of a function decorated by "
210
+ "'run_on_buffered_las' is expected to be the path to an existing input file."
211
+ )
212
+
213
+ if not Path(output_file).parent.is_dir():
214
+ raise FileNotFoundError(
215
+ f"Parent folder for file {args[1]} not found. The second argument of a function "
216
+ "decorated by 'run_on_buffered_las' is expected to be the path to an output "
217
+ "file in an existing folder"
218
+ )
219
+
220
+ with (
221
+ tempfile.NamedTemporaryFile(suffix="_buffered_input.laz", dir=".") as buf_in,
222
+ tempfile.NamedTemporaryFile(suffix="_buffered_output.laz", dir=".") as buf_out,
223
+ ):
224
+ create_las_with_buffer(
225
+ Path(input_file).parent,
226
+ input_file,
227
+ buf_in.name,
228
+ buffer_width=buffer_width,
229
+ spatial_ref=spatial_ref,
230
+ tile_width=tile_width,
231
+ tile_coord_scale=tile_coord_scale,
232
+ tag_original_tile=True,
233
+ )
234
+ func(buf_in.name, buf_out.name, *args[2:], **kwargs)
235
+
236
+ remove_points_from_buffer(buf_out.name, output_file)
237
+
238
+ return
239
+
240
+ return wrapper
241
+
242
+ return decorator
243
+
244
+
119
245
  def parse_args():
120
246
  parser = argparse.ArgumentParser("Add a buffer to a las tile by stitching with its neighbors")
121
247
  parser.add_argument(
@@ -0,0 +1,58 @@
1
+ import argparse
2
+ import os
3
+
4
+ import pdal
5
+ from pdaltools.las_info import get_writer_parameters_from_reader_metadata
6
+
7
+ def remove_dimensions_from_las(input_las: str, dimensions: [str], output_las: str):
8
+ """
9
+ export new las without some dimensions
10
+ """
11
+ pipeline = pdal.Pipeline() | pdal.Reader.las(input_las)
12
+ pipeline.execute()
13
+ points = pipeline.arrays[0]
14
+ input_dimensions = list(points.dtype.fields.keys())
15
+ output_dimensions = [dim for dim in input_dimensions if dim not in dimensions]
16
+ points_pruned = points[output_dimensions]
17
+ params = get_writer_parameters_from_reader_metadata(pipeline.metadata)
18
+ pipeline_end = pdal.Pipeline(arrays=[points_pruned])
19
+ pipeline_end |= pdal.Writer.las(output_las, forward="all", **params)
20
+ pipeline_end.execute()
21
+
22
+
23
+ def parse_args():
24
+ parser = argparse.ArgumentParser("Remove dimensions from las")
25
+ parser.add_argument(
26
+ "--input_las",
27
+ "-i",
28
+ type=str,
29
+ required=True,
30
+ help="Path to the the las for which the dimensions will be removed",
31
+ )
32
+ parser.add_argument(
33
+ "--output_las",
34
+ "-o",
35
+ type=str,
36
+ required=False,
37
+ help="Path to the the output las ; if none, we replace the input las",
38
+ )
39
+ parser.add_argument(
40
+ "--dimensions",
41
+ "-d",
42
+ type=str,
43
+ required=True,
44
+ nargs="+",
45
+ help="The dimension we would like to remove from the point cloud file ; be aware to not remove mandatory "
46
+ "dimensions of las"
47
+ )
48
+
49
+ return parser.parse_args()
50
+
51
+
52
+ if __name__ == "__main__":
53
+ args = parse_args()
54
+ remove_dimensions_from_las(
55
+ input_las=args.input_las,
56
+ dimensions=args.dimensions,
57
+ output_las=args.input_las if args.output_las is None else args.output_las,
58
+ )