ign-pdal-tools 1.6.0__py3-none-any.whl → 1.7.1__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.
- {ign_pdal_tools-1.6.0.dist-info → ign_pdal_tools-1.7.1.dist-info}/METADATA +1 -1
- {ign_pdal_tools-1.6.0.dist-info → ign_pdal_tools-1.7.1.dist-info}/RECORD +8 -7
- {ign_pdal_tools-1.6.0.dist-info → ign_pdal_tools-1.7.1.dist-info}/WHEEL +1 -1
- pdaltools/_version.py +1 -1
- pdaltools/las_add_buffer.py +147 -21
- pdaltools/las_remove_dimensions.py +58 -0
- {ign_pdal_tools-1.6.0.dist-info → ign_pdal_tools-1.7.1.dist-info}/LICENSE.md +0 -0
- {ign_pdal_tools-1.6.0.dist-info → ign_pdal_tools-1.7.1.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
pdaltools/_version.py,sha256=
|
|
1
|
+
pdaltools/_version.py,sha256=xwBg7KNoeyzwyonEgM9Mm-9CRDfrRb3V1tY-iPeL3Os,74
|
|
2
2
|
pdaltools/color.py,sha256=7U-SThIKqrfE1xXXnFqpbIhmZEqna29nRiyLW8l8Y1c,8075
|
|
3
|
-
pdaltools/las_add_buffer.py,sha256=
|
|
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.
|
|
11
|
-
ign_pdal_tools-1.
|
|
12
|
-
ign_pdal_tools-1.
|
|
13
|
-
ign_pdal_tools-1.
|
|
14
|
-
ign_pdal_tools-1.
|
|
11
|
+
ign_pdal_tools-1.7.1.dist-info/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
|
|
12
|
+
ign_pdal_tools-1.7.1.dist-info/METADATA,sha256=4xHIuVdU1-CJuSZZZhUIWUGg1rLzTtq9JV_VRsvrBPQ,4825
|
|
13
|
+
ign_pdal_tools-1.7.1.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
|
|
14
|
+
ign_pdal_tools-1.7.1.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
|
|
15
|
+
ign_pdal_tools-1.7.1.dist-info/RECORD,,
|
pdaltools/_version.py
CHANGED
pdaltools/las_add_buffer.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
-
|
|
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
|
|
28
|
-
tile_filename (str):
|
|
29
|
-
output_filename (str)
|
|
30
|
-
buffer_width (int): width of the border to add to the tile (in
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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)
|
|
77
|
-
spatial_ref (str): spatial reference for the writer
|
|
78
|
-
|
|
79
|
-
tile_coord_scale (int)
|
|
80
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
)
|
|
File without changes
|
|
File without changes
|