ign-pdal-tools 1.7.3__tar.gz → 1.7.5__tar.gz
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.7.3 → ign_pdal_tools-1.7.5}/PKG-INFO +6 -1
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/README.md +5 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/PKG-INFO +6 -1
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/SOURCES.txt +2 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/_version.py +1 -1
- ign_pdal_tools-1.7.5/pdaltools/add_points_in_las.py +104 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/color.py +13 -9
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_info.py +27 -1
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_remove_dimensions.py +2 -2
- ign_pdal_tools-1.7.5/pdaltools/pcd_info.py +76 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/standardize_format.py +31 -11
- ign_pdal_tools-1.7.5/test/test_add_points_in_las.py +72 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_info.py +5 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_remove_dimensions.py +11 -6
- ign_pdal_tools-1.7.5/test/test_pcd_info.py +87 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_standardize_format.py +50 -9
- ign_pdal_tools-1.7.3/pdaltools/pcd_info.py +0 -46
- ign_pdal_tools-1.7.3/test/test_pcd_info.py +0 -61
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/LICENSE.md +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/dependency_links.txt +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/top_level.txt +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_add_buffer.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_clip.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_merge.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/replace_attribute_in_las.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/unlock_file.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pyproject.toml +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/setup.cfg +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_color.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_add_buffer.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_clip.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_merge.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_replace_attribute_in_las.py +0 -0
- {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_unlock.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ign-pdal-tools
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.5
|
|
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
|
|
@@ -87,6 +87,11 @@ By default, `xcoord` and `ycoord` are given in kilometers and the shape of the t
|
|
|
87
87
|
`readers.las: Global encoding WKT flag not set for point format 6 - 10.` which is due to TerraSolid
|
|
88
88
|
malformed LAS output for LAS1.4 files with point format 6 to 10.
|
|
89
89
|
|
|
90
|
+
## Add points in Las
|
|
91
|
+
|
|
92
|
+
[add_points_in_las.py](pdaltools/add_points_in_las.py): add points from some vector files (ex: shp, geojson, ...) inside Las. New points will have X,Y and Z coordinates. Other attributes values given by the initial las file are null (ex: classification at 0). These others attributes could be forced by using the '--dimensions/-d' option in the command line (ex : 'add_points_in_las.py -i myLas.las -g myPoints.json -d classification=64' - points will have their classification set to 64). The dimension should be present in the initial las ; this is not allowed to add new dimension.
|
|
93
|
+
|
|
94
|
+
|
|
90
95
|
# Dev / Build
|
|
91
96
|
|
|
92
97
|
## Contribute
|
|
@@ -79,6 +79,11 @@ By default, `xcoord` and `ycoord` are given in kilometers and the shape of the t
|
|
|
79
79
|
`readers.las: Global encoding WKT flag not set for point format 6 - 10.` which is due to TerraSolid
|
|
80
80
|
malformed LAS output for LAS1.4 files with point format 6 to 10.
|
|
81
81
|
|
|
82
|
+
## Add points in Las
|
|
83
|
+
|
|
84
|
+
[add_points_in_las.py](pdaltools/add_points_in_las.py): add points from some vector files (ex: shp, geojson, ...) inside Las. New points will have X,Y and Z coordinates. Other attributes values given by the initial las file are null (ex: classification at 0). These others attributes could be forced by using the '--dimensions/-d' option in the command line (ex : 'add_points_in_las.py -i myLas.las -g myPoints.json -d classification=64' - points will have their classification set to 64). The dimension should be present in the initial las ; this is not allowed to add new dimension.
|
|
85
|
+
|
|
86
|
+
|
|
82
87
|
# Dev / Build
|
|
83
88
|
|
|
84
89
|
## Contribute
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ign-pdal-tools
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.5
|
|
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
|
|
@@ -87,6 +87,11 @@ By default, `xcoord` and `ycoord` are given in kilometers and the shape of the t
|
|
|
87
87
|
`readers.las: Global encoding WKT flag not set for point format 6 - 10.` which is due to TerraSolid
|
|
88
88
|
malformed LAS output for LAS1.4 files with point format 6 to 10.
|
|
89
89
|
|
|
90
|
+
## Add points in Las
|
|
91
|
+
|
|
92
|
+
[add_points_in_las.py](pdaltools/add_points_in_las.py): add points from some vector files (ex: shp, geojson, ...) inside Las. New points will have X,Y and Z coordinates. Other attributes values given by the initial las file are null (ex: classification at 0). These others attributes could be forced by using the '--dimensions/-d' option in the command line (ex : 'add_points_in_las.py -i myLas.las -g myPoints.json -d classification=64' - points will have their classification set to 64). The dimension should be present in the initial las ; this is not allowed to add new dimension.
|
|
93
|
+
|
|
94
|
+
|
|
90
95
|
# Dev / Build
|
|
91
96
|
|
|
92
97
|
## Contribute
|
|
@@ -6,6 +6,7 @@ ign_pdal_tools.egg-info/SOURCES.txt
|
|
|
6
6
|
ign_pdal_tools.egg-info/dependency_links.txt
|
|
7
7
|
ign_pdal_tools.egg-info/top_level.txt
|
|
8
8
|
pdaltools/_version.py
|
|
9
|
+
pdaltools/add_points_in_las.py
|
|
9
10
|
pdaltools/color.py
|
|
10
11
|
pdaltools/las_add_buffer.py
|
|
11
12
|
pdaltools/las_clip.py
|
|
@@ -16,6 +17,7 @@ pdaltools/pcd_info.py
|
|
|
16
17
|
pdaltools/replace_attribute_in_las.py
|
|
17
18
|
pdaltools/standardize_format.py
|
|
18
19
|
pdaltools/unlock_file.py
|
|
20
|
+
test/test_add_points_in_las.py
|
|
19
21
|
test/test_color.py
|
|
20
22
|
test/test_las_add_buffer.py
|
|
21
23
|
test/test_las_clip.py
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import geopandas
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pdal
|
|
6
|
+
|
|
7
|
+
from pdaltools.las_info import get_writer_parameters_from_reader_metadata, las_info_metadata, get_bounds_from_header_info
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def extract_points_from_geo(input_geo: str):
|
|
11
|
+
file = open(input_geo)
|
|
12
|
+
df = geopandas.read_file(file)
|
|
13
|
+
return df.get_coordinates(ignore_index=True, include_z=True)
|
|
14
|
+
|
|
15
|
+
def point_in_bound(bound_minx, bound_maxx, bound_miny, bound_maxy, pt_x, pt_y):
|
|
16
|
+
return pt_x >= bound_minx and pt_x <= bound_maxx and pt_y >= bound_miny and pt_y <= bound_maxy
|
|
17
|
+
|
|
18
|
+
def add_points_in_las(input_las: str, input_geo: str, output_las: str, inside_las: bool, values_dimensions: {}):
|
|
19
|
+
points_geo = extract_points_from_geo(input_geo)
|
|
20
|
+
pipeline = pdal.Pipeline() | pdal.Reader.las(input_las)
|
|
21
|
+
pipeline.execute()
|
|
22
|
+
points_las = pipeline.arrays[0]
|
|
23
|
+
dimensions = list(points_las.dtype.fields.keys())
|
|
24
|
+
|
|
25
|
+
if inside_las:
|
|
26
|
+
mtd = las_info_metadata(input_las)
|
|
27
|
+
bound_minx, bound_maxx, bound_miny, bound_maxy = get_bounds_from_header_info(mtd)
|
|
28
|
+
|
|
29
|
+
for i in points_geo.index:
|
|
30
|
+
if inside_las :
|
|
31
|
+
if not point_in_bound(bound_minx, bound_maxx, bound_miny, bound_maxy, points_geo["x"][i], points_geo["y"][i]):
|
|
32
|
+
continue
|
|
33
|
+
pt_las = np.empty(1, dtype=points_las.dtype)
|
|
34
|
+
pt_las[0][dimensions.index("X")] = points_geo["x"][i]
|
|
35
|
+
pt_las[0][dimensions.index("Y")] = points_geo["y"][i]
|
|
36
|
+
pt_las[0][dimensions.index("Z")] = points_geo["z"][i]
|
|
37
|
+
for val in values_dimensions:
|
|
38
|
+
pt_las[0][dimensions.index(val)] = values_dimensions[val]
|
|
39
|
+
points_las = np.append(points_las, pt_las, axis=0)
|
|
40
|
+
|
|
41
|
+
params = get_writer_parameters_from_reader_metadata(pipeline.metadata)
|
|
42
|
+
pipeline_end = pdal.Pipeline(arrays=[points_las])
|
|
43
|
+
pipeline_end |= pdal.Writer.las(output_las, forward="all", **params)
|
|
44
|
+
pipeline_end.execute()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_args():
|
|
48
|
+
parser = argparse.ArgumentParser("Add points from geometry file in a las/laz file.")
|
|
49
|
+
parser.add_argument("--input_file", "-i", type=str, help="Las/Laz input file")
|
|
50
|
+
parser.add_argument("--output_file", "-o", type=str, help="Las/Laz output file.")
|
|
51
|
+
parser.add_argument("--input_geo_file", "-g", type=str, help="Geometry input file.")
|
|
52
|
+
parser.add_argument("--inside_las", "-l", type=str, help="Keep points only inside the las boundary.")
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--dimensions",
|
|
55
|
+
"-d",
|
|
56
|
+
metavar="KEY=VALUE",
|
|
57
|
+
nargs="+",
|
|
58
|
+
help="Set a number of key-value pairs corresponding to value "
|
|
59
|
+
"needed in points added in the output las; key should be included in the input las.",
|
|
60
|
+
)
|
|
61
|
+
return parser.parse_args()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_nature(value, nature):
|
|
65
|
+
if value is None:
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
68
|
+
nature(value)
|
|
69
|
+
return True
|
|
70
|
+
except:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_var(s):
|
|
75
|
+
items = s.split("=")
|
|
76
|
+
key = items[0].strip()
|
|
77
|
+
if len(items) > 1:
|
|
78
|
+
value = "=".join(items[1:])
|
|
79
|
+
if is_nature(value, int):
|
|
80
|
+
value = int(value)
|
|
81
|
+
elif is_nature(value, float):
|
|
82
|
+
value = float(value)
|
|
83
|
+
return (key, value)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_vars(items):
|
|
87
|
+
d = {}
|
|
88
|
+
if items:
|
|
89
|
+
for item in items:
|
|
90
|
+
key, value = parse_var(item)
|
|
91
|
+
d[key] = value
|
|
92
|
+
return d
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
args = parse_args()
|
|
97
|
+
added_dimensions = parse_vars(args.dimensions)
|
|
98
|
+
add_points_in_las(
|
|
99
|
+
input_las=args.input_file,
|
|
100
|
+
input_geo=args.input_geo_file,
|
|
101
|
+
output_las=args.input_file if args.output_file is None else args.output_file,
|
|
102
|
+
inside_las=args.inside_las,
|
|
103
|
+
values_dimensions=added_dimensions,
|
|
104
|
+
)
|
|
@@ -69,11 +69,11 @@ def is_image_white(filename: str):
|
|
|
69
69
|
def download_image_from_geoplateforme(
|
|
70
70
|
proj, layer, minx, miny, maxx, maxy, pixel_per_meter, outfile, timeout, check_images
|
|
71
71
|
):
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
# Force a 1-pixel margin in the east and south borders
|
|
73
|
+
# to make sure that no point of the pointcloud is on the limit of the last pixel
|
|
74
|
+
# to prevent interpolation issues
|
|
75
|
+
maxx = maxx + 1 / pixel_per_meter
|
|
76
|
+
miny = miny - 1 / pixel_per_meter
|
|
77
77
|
|
|
78
78
|
# for layer in layers:
|
|
79
79
|
URL_GPP = "https://data.geopf.fr/wms-r/wms?"
|
|
@@ -136,22 +136,26 @@ def color(
|
|
|
136
136
|
|
|
137
137
|
tmp_ortho = None
|
|
138
138
|
if color_rvb_enabled:
|
|
139
|
-
tmp_ortho = tempfile.NamedTemporaryFile()
|
|
139
|
+
tmp_ortho = tempfile.NamedTemporaryFile(suffix="_rvb.tif")
|
|
140
140
|
download_image_from_geoplateforme_retrying(
|
|
141
141
|
proj, stream_RGB, minx, miny, maxx, maxy, pixel_per_meter, tmp_ortho.name, timeout_second, check_images
|
|
142
142
|
)
|
|
143
|
-
|
|
143
|
+
# Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
|
|
144
|
+
# which turns it to a 0 to 255*256 range.
|
|
145
|
+
# It is kept this way because of other dependencies that have been tuned to fit this range
|
|
144
146
|
pipeline |= pdal.Filter.colorization(
|
|
145
147
|
raster=tmp_ortho.name, dimensions="Red:1:256.0, Green:2:256.0, Blue:3:256.0"
|
|
146
148
|
)
|
|
147
149
|
|
|
148
150
|
tmp_ortho_irc = None
|
|
149
151
|
if color_ir_enabled:
|
|
150
|
-
tmp_ortho_irc = tempfile.NamedTemporaryFile()
|
|
152
|
+
tmp_ortho_irc = tempfile.NamedTemporaryFile(suffix="_irc.tif")
|
|
151
153
|
download_image_from_geoplateforme_retrying(
|
|
152
154
|
proj, stream_IRC, minx, miny, maxx, maxy, pixel_per_meter, tmp_ortho_irc.name, timeout_second, check_images
|
|
153
155
|
)
|
|
154
|
-
|
|
156
|
+
# Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
|
|
157
|
+
# which turns it to a 0 to 255*256 range.
|
|
158
|
+
# It is kept this way because of other dependencies that have been tuned to fit this range
|
|
155
159
|
pipeline |= pdal.Filter.colorization(raster=tmp_ortho_irc.name, dimensions="Infrared:1:256.0")
|
|
156
160
|
|
|
157
161
|
pipeline |= pdal.Writer.las(
|
|
@@ -6,6 +6,8 @@ from typing import Dict, Tuple
|
|
|
6
6
|
import osgeo.osr as osr
|
|
7
7
|
import pdal
|
|
8
8
|
|
|
9
|
+
from pdaltools.pcd_info import infer_tile_origin
|
|
10
|
+
|
|
9
11
|
osr.UseExceptions()
|
|
10
12
|
|
|
11
13
|
|
|
@@ -17,13 +19,37 @@ def las_info_metadata(filename: str):
|
|
|
17
19
|
return metadata
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
def get_bounds_from_header_info(metadata):
|
|
22
|
+
def get_bounds_from_header_info(metadata: Dict) -> Tuple[float, float, float, float]:
|
|
23
|
+
"""Get bounds from metadata that has been extracted previously from the header of a las file
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
metadata (str): Dictonary containing metadata from a las file (as extracted with pipeline.quickinfo)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple[float, float, float, float]: minx, maxx, miny, maxy
|
|
30
|
+
"""
|
|
21
31
|
bounds = metadata["bounds"]
|
|
22
32
|
minx, maxx, miny, maxy = bounds["minx"], bounds["maxx"], bounds["miny"], bounds["maxy"]
|
|
23
33
|
|
|
24
34
|
return minx, maxx, miny, maxy
|
|
25
35
|
|
|
26
36
|
|
|
37
|
+
def get_tile_origin_using_header_info(filename: str, tile_width: int = 1000) -> Tuple[int, int]:
|
|
38
|
+
""" "Get las file theoretical origin (xmin, ymax) for a data that originates from a square tesselation/tiling
|
|
39
|
+
using the tesselation tile width only, directly from its path
|
|
40
|
+
Args:
|
|
41
|
+
filename (str): path to the las file
|
|
42
|
+
tile_width (int, optional): Tesselation tile width (in meters). Defaults to 1000.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Tuple[int, int]: (origin_x, origin_y) tile origin coordinates = theoretical (xmin, ymax)
|
|
46
|
+
"""
|
|
47
|
+
metadata = las_info_metadata(filename)
|
|
48
|
+
minx, maxx, miny, maxy = get_bounds_from_header_info(metadata)
|
|
49
|
+
|
|
50
|
+
return infer_tile_origin(minx, maxx, miny, maxy, tile_width)
|
|
51
|
+
|
|
52
|
+
|
|
27
53
|
def get_epsg_from_header_info(metadata):
|
|
28
54
|
if "srs" not in metadata.keys():
|
|
29
55
|
raise RuntimeError("EPSG could not be inferred from metadata: No 'srs' key in metadata.")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import os
|
|
3
2
|
|
|
4
3
|
import pdal
|
|
5
4
|
from pdaltools.las_info import get_writer_parameters_from_reader_metadata
|
|
6
5
|
|
|
6
|
+
|
|
7
7
|
def remove_dimensions_from_las(input_las: str, dimensions: [str], output_las: str):
|
|
8
8
|
"""
|
|
9
9
|
export new las without some dimensions
|
|
@@ -43,7 +43,7 @@ def parse_args():
|
|
|
43
43
|
required=True,
|
|
44
44
|
nargs="+",
|
|
45
45
|
help="The dimension we would like to remove from the point cloud file ; be aware to not remove mandatory "
|
|
46
|
-
|
|
46
|
+
"dimensions of las",
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
return parser.parse_args()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tools to get information from a point cloud (points as a numpy array)"""
|
|
2
|
+
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def infer_tile_origin(minx: float, maxx: float, miny: float, maxy: float, tile_width: int) -> Tuple[int, int]:
|
|
9
|
+
"""Get point cloud theoretical origin (xmin, ymax) for a data that originates from a square tesselation/tiling
|
|
10
|
+
using the tesselation tile width only, based on the min/max values
|
|
11
|
+
|
|
12
|
+
Edge values are supposed to be included in the tile
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
minx (float): point cloud min x value
|
|
16
|
+
maxx (float): point cloud max x value
|
|
17
|
+
miny (float): point cloud min y value
|
|
18
|
+
maxy (float): point cloud max y value
|
|
19
|
+
tile_width (int): tile width in meters
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: In case the min and max values do not belong to the same tile
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Tuple[int, int]: (origin_x, origin_y) tile origin coordinates = theoretical (xmin, ymax)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
minx_tile_index = np.floor(minx / tile_width)
|
|
29
|
+
maxx_tile_index = np.floor(maxx / tile_width) if maxx % tile_width != 0 else np.floor(maxx / tile_width) - 1
|
|
30
|
+
miny_tile_index = np.ceil(miny / tile_width) if miny % tile_width != 0 else np.floor(miny / tile_width) + 1
|
|
31
|
+
maxy_tile_index = np.ceil(maxy / tile_width)
|
|
32
|
+
|
|
33
|
+
if maxx_tile_index == minx_tile_index and maxy_tile_index == miny_tile_index:
|
|
34
|
+
origin_x = minx_tile_index * tile_width
|
|
35
|
+
origin_y = maxy_tile_index * tile_width
|
|
36
|
+
return origin_x, origin_y
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Min values (x={minx} and y={miny}) do not belong to the same theoretical tile as"
|
|
40
|
+
f"max values (x={maxx} and y={maxy})."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_pointcloud_origin_from_tile_width(
|
|
45
|
+
points: np.ndarray, tile_width: int = 1000, buffer_size: float = 0
|
|
46
|
+
) -> Tuple[int, int]:
|
|
47
|
+
"""Get point cloud theoretical origin (xmin, ymax) for a data that originates from a square tesselation/tiling
|
|
48
|
+
using the tesselation tile width only, based on the point cloud as a np.ndarray
|
|
49
|
+
|
|
50
|
+
Edge values are supposed to be included in the tile
|
|
51
|
+
|
|
52
|
+
In case buffer_size is provided, the origin will be calculated on an "original" tile, supposing that
|
|
53
|
+
there has been a buffer added to the input tile.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
points (np.ndarray): numpy array with the tile points
|
|
57
|
+
tile_width (int, optional): Edge size of the square used for tiling. Defaults to 1000.
|
|
58
|
+
buffer_size (float, optional): Optional buffer around the tile. Defaults to 0.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: Raise an error when the initial tile is smaller than the buffer (in this case, we cannot find the
|
|
62
|
+
origin (it can be either in the buffer or in the tile))
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple[int, int]: (origin_x, origin_y) origin coordinates
|
|
66
|
+
"""
|
|
67
|
+
# Extract coordinates xmin, xmax, ymin and ymax of the original tile without buffer
|
|
68
|
+
minx, miny = np.min(points[:, :2], axis=0) + buffer_size
|
|
69
|
+
maxx, maxy = np.max(points[:, :2], axis=0) - buffer_size
|
|
70
|
+
|
|
71
|
+
if maxx < minx or maxy < miny:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"Cannot find pointcloud origin as the pointcloud width or height is smaller than buffer width"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return infer_tile_origin(minx, maxx, miny, maxy, tile_width)
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
import argparse
|
|
12
12
|
import os
|
|
13
|
+
import platform
|
|
13
14
|
import subprocess as sp
|
|
14
15
|
import tempfile
|
|
15
|
-
from typing import Dict
|
|
16
|
+
from typing import Dict, List
|
|
16
17
|
|
|
17
18
|
import pdal
|
|
18
19
|
|
|
19
20
|
from pdaltools.unlock_file import copy_and_hack_decorator
|
|
20
21
|
|
|
22
|
+
# Standard parameters to pass to the pdal writer
|
|
21
23
|
STANDARD_PARAMETERS = dict(
|
|
22
24
|
major_version="1",
|
|
23
25
|
minor_version="4", # Laz format version (pdal always write in 1.x format)
|
|
@@ -43,6 +45,13 @@ def parse_args():
|
|
|
43
45
|
"--record_format", choices=[6, 8], type=int, help="Record format: 6 (no color) or 8 (4 color channels)"
|
|
44
46
|
)
|
|
45
47
|
parser.add_argument("--projection", default="EPSG:2154", type=str, help="Projection, eg. EPSG:2154")
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--class_points_removed",
|
|
50
|
+
default=[],
|
|
51
|
+
nargs="*",
|
|
52
|
+
type=str,
|
|
53
|
+
help="List of classes number. Points of this classes will be removed from the file",
|
|
54
|
+
)
|
|
46
55
|
parser.add_argument(
|
|
47
56
|
"--extra_dims",
|
|
48
57
|
default=[],
|
|
@@ -51,7 +60,6 @@ def parse_args():
|
|
|
51
60
|
help="List of extra dims to keep in the output (default=[], use 'all' to keep all extra dims), "
|
|
52
61
|
"extra_dims must be specified with their type (see pdal.writers.las documentation, eg 'dim1=double')",
|
|
53
62
|
)
|
|
54
|
-
|
|
55
63
|
return parser.parse_args()
|
|
56
64
|
|
|
57
65
|
|
|
@@ -61,20 +69,28 @@ def get_writer_parameters(new_parameters: Dict) -> Dict:
|
|
|
61
69
|
override the standard ones
|
|
62
70
|
"""
|
|
63
71
|
params = STANDARD_PARAMETERS | new_parameters
|
|
64
|
-
|
|
65
72
|
return params
|
|
66
73
|
|
|
67
74
|
|
|
68
|
-
def rewrite_with_pdal(
|
|
69
|
-
|
|
75
|
+
def rewrite_with_pdal(
|
|
76
|
+
input_file: str, output_file: str, params_from_parser: Dict, classes_to_remove: List = []
|
|
77
|
+
) -> None:
|
|
70
78
|
params = get_writer_parameters(params_from_parser)
|
|
71
|
-
pipeline = pdal.
|
|
79
|
+
pipeline = pdal.Pipeline()
|
|
80
|
+
pipeline |= pdal.Reader.las(input_file)
|
|
81
|
+
if classes_to_remove:
|
|
82
|
+
expression = "&&".join([f"Classification != {c}" for c in classes_to_remove])
|
|
83
|
+
pipeline |= pdal.Filter.expression(expression=expression)
|
|
72
84
|
pipeline |= pdal.Writer(filename=output_file, forward="all", **params)
|
|
73
85
|
pipeline.execute()
|
|
74
86
|
|
|
75
87
|
|
|
76
88
|
def exec_las2las(input_file: str, output_file: str):
|
|
77
|
-
|
|
89
|
+
if platform.processor() == "arm" and platform.architecture()[0] == "64bit":
|
|
90
|
+
las2las = "las2las64"
|
|
91
|
+
else:
|
|
92
|
+
las2las = "las2las"
|
|
93
|
+
r = sp.run([las2las, "-i", input_file, "-o", output_file], stderr=sp.PIPE, stdout=sp.PIPE)
|
|
78
94
|
if r.returncode == 1:
|
|
79
95
|
msg = r.stderr.decode()
|
|
80
96
|
print(msg)
|
|
@@ -86,14 +102,18 @@ def exec_las2las(input_file: str, output_file: str):
|
|
|
86
102
|
|
|
87
103
|
|
|
88
104
|
@copy_and_hack_decorator
|
|
89
|
-
def standardize(input_file: str, output_file: str, params_from_parser: Dict) -> None:
|
|
105
|
+
def standardize(input_file: str, output_file: str, params_from_parser: Dict, class_points_removed: []) -> None:
|
|
90
106
|
filename = os.path.basename(output_file)
|
|
91
107
|
with tempfile.NamedTemporaryFile(suffix=filename) as tmp:
|
|
92
|
-
rewrite_with_pdal(input_file, tmp.name, params_from_parser)
|
|
108
|
+
rewrite_with_pdal(input_file, tmp.name, params_from_parser, class_points_removed)
|
|
93
109
|
exec_las2las(tmp.name, output_file)
|
|
94
110
|
|
|
95
111
|
|
|
96
112
|
if __name__ == "__main__":
|
|
97
113
|
args = parse_args()
|
|
98
|
-
params_from_parser = dict(
|
|
99
|
-
|
|
114
|
+
params_from_parser = dict(
|
|
115
|
+
dataformat_id=args.record_format,
|
|
116
|
+
a_srs=args.projection,
|
|
117
|
+
extra_dims=args.extra_dims,
|
|
118
|
+
)
|
|
119
|
+
standardize(args.input_file, args.output_file, params_from_parser, args.class_points_removed)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
import random as rand
|
|
4
|
+
import tempfile
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import pdal
|
|
8
|
+
|
|
9
|
+
import geopandas as gpd
|
|
10
|
+
from shapely.geometry import Point
|
|
11
|
+
|
|
12
|
+
from pdaltools import add_points_in_las
|
|
13
|
+
|
|
14
|
+
numeric_precision = 4
|
|
15
|
+
|
|
16
|
+
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
INPUT_DIR = os.path.join(TEST_PATH, "data")
|
|
18
|
+
INPUT_LAS = os.path.join(INPUT_DIR, "test_data_77055_627760_LA93_IGN69.laz")
|
|
19
|
+
|
|
20
|
+
Xmin = 770575
|
|
21
|
+
Ymin = 6277575
|
|
22
|
+
Zmin = 20
|
|
23
|
+
Size = 20
|
|
24
|
+
|
|
25
|
+
def distance3D(pt_geo, pt_las):
|
|
26
|
+
return round(
|
|
27
|
+
math.sqrt((pt_geo.x - pt_las['X']) ** 2 + (pt_geo.y - pt_las['Y']) ** 2 + (pt_geo.z - pt_las['Z']) ** 2),
|
|
28
|
+
numeric_precision,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def add_point_in_las(pt_geo, inside_las):
|
|
32
|
+
geom = [pt_geo]
|
|
33
|
+
series = gpd.GeoSeries(geom, crs="2154")
|
|
34
|
+
|
|
35
|
+
with tempfile.NamedTemporaryFile(suffix="_geom_tmp.las") as out_las_file:
|
|
36
|
+
with tempfile.NamedTemporaryFile(suffix="_geom_tmp.geojson") as geom_file:
|
|
37
|
+
series.to_file(geom_file.name)
|
|
38
|
+
|
|
39
|
+
added_dimensions = {"Classification":64, "Intensity":1.}
|
|
40
|
+
add_points_in_las.add_points_in_las(INPUT_LAS, geom_file.name, out_las_file.name, inside_las, added_dimensions)
|
|
41
|
+
|
|
42
|
+
pipeline = pdal.Pipeline() | pdal.Reader.las(out_las_file.name)
|
|
43
|
+
pipeline.execute()
|
|
44
|
+
points_las = pipeline.arrays[0]
|
|
45
|
+
points_las = [e for e in points_las if all(e[val] == added_dimensions[val] for val in added_dimensions)]
|
|
46
|
+
return points_las
|
|
47
|
+
|
|
48
|
+
def test_add_point_inside_las():
|
|
49
|
+
X = Xmin + rand.uniform(0, 1) * Size
|
|
50
|
+
Y = Ymin + rand.uniform(0, 1) * Size
|
|
51
|
+
Z = Zmin + rand.uniform(0, 1) * 10
|
|
52
|
+
pt_geo = Point(X, Y, Z)
|
|
53
|
+
points_las = add_point_in_las(pt_geo=pt_geo, inside_las=True)
|
|
54
|
+
assert len(points_las) == 1
|
|
55
|
+
assert distance3D(pt_geo, points_las[0]) < 1 / numeric_precision
|
|
56
|
+
|
|
57
|
+
def test_add_point_outside_las_no_control():
|
|
58
|
+
X = Xmin + rand.uniform(2, 3) * Size
|
|
59
|
+
Y = Ymin + rand.uniform(0, 1) * Size
|
|
60
|
+
Z = Zmin + rand.uniform(0, 1) * 10
|
|
61
|
+
pt_geo = Point(X, Y, Z)
|
|
62
|
+
points_las = add_point_in_las(pt_geo=pt_geo, inside_las=False)
|
|
63
|
+
assert len(points_las) == 1
|
|
64
|
+
assert distance3D(pt_geo, points_las[0]) < 1 / numeric_precision
|
|
65
|
+
|
|
66
|
+
def test_add_point_outside_las_with_control():
|
|
67
|
+
X = Xmin + rand.uniform(2, 3) * Size
|
|
68
|
+
Y = Ymin + rand.uniform(2, 3) * Size
|
|
69
|
+
Z = Zmin + rand.uniform(0, 1) * 10
|
|
70
|
+
pt_geo = Point(X, Y, Z)
|
|
71
|
+
points_las = add_point_in_las(pt_geo=pt_geo, inside_las=True)
|
|
72
|
+
assert len(points_las) == 0
|
|
@@ -40,6 +40,11 @@ def test_get_bounds_from_quickinfo_metadata():
|
|
|
40
40
|
assert bounds == (INPUT_MINS[0], INPUT_MAXS[0], INPUT_MINS[1], INPUT_MAXS[1])
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def test_get_tile_origin_using_header_info():
|
|
44
|
+
origin_x, origin_y = las_info.get_tile_origin_using_header_info(INPUT_FILE, tile_width=TILE_WIDTH)
|
|
45
|
+
assert (origin_x, origin_y) == (COORD_X * TILE_COORD_SCALE, COORD_Y * TILE_COORD_SCALE)
|
|
46
|
+
|
|
47
|
+
|
|
43
48
|
def test_get_epsg_from_quickinfo_metadata_ok():
|
|
44
49
|
metadata = las_info.las_info_metadata(INPUT_FILE)
|
|
45
50
|
assert las_info.get_epsg_from_header_info(metadata) == "2154"
|
|
@@ -13,16 +13,22 @@ INPUT_DIR = os.path.join(TEST_PATH, "data")
|
|
|
13
13
|
ini_las = os.path.join(INPUT_DIR, "test_data_77055_627760_LA93_IGN69.laz")
|
|
14
14
|
added_dimensions = ["DIM_1", "DIM_2"]
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
def get_points(input_las: str):
|
|
17
18
|
pipeline_read_ini = pdal.Pipeline() | pdal.Reader.las(input_las)
|
|
18
19
|
pipeline_read_ini.execute()
|
|
19
20
|
return pipeline_read_ini.arrays[0]
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
|
|
23
|
+
def append_dimension(input_las: str, output_las: str):
|
|
22
24
|
pipeline = pdal.Pipeline()
|
|
23
25
|
pipeline |= pdal.Reader.las(input_las)
|
|
24
26
|
pipeline |= pdal.Filter.ferry(dimensions="=>" + ", =>".join(added_dimensions))
|
|
25
|
-
pipeline |= pdal.Writer.las(
|
|
27
|
+
pipeline |= pdal.Writer.las(
|
|
28
|
+
output_las,
|
|
29
|
+
extra_dims="all",
|
|
30
|
+
forward="all",
|
|
31
|
+
)
|
|
26
32
|
pipeline.execute()
|
|
27
33
|
|
|
28
34
|
|
|
@@ -52,10 +58,9 @@ def test_remove_one_dimension():
|
|
|
52
58
|
las_remove_dimensions.remove_dimensions_from_las(tmp_las.name, ["DIM_1"], tmp_las_rm.name)
|
|
53
59
|
points_end = get_points(tmp_las_rm.name)
|
|
54
60
|
|
|
55
|
-
assert list(points_end.dtype.fields.keys()).index("DIM_2") >= 0# should still contains DIM_2
|
|
61
|
+
assert list(points_end.dtype.fields.keys()).index("DIM_2") >= 0 # should still contains DIM_2
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
list(points_end.dtype.fields.keys()).index("DIM_1") # should not have DIM_1
|
|
63
|
+
assert "DIM_1" not in points_end.dtype.fields.keys(), "LAS should not have dimension DIM_1"
|
|
59
64
|
|
|
60
65
|
with pytest.raises(TypeError):
|
|
61
66
|
numpy.array_equal(points_ini, points_end) # output data should not be the same
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import laspy
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from pdaltools import pcd_info
|
|
8
|
+
|
|
9
|
+
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
+
TMP_PATH = os.path.join(TEST_PATH, "tmp")
|
|
11
|
+
DATA_PATH = os.path.join(TEST_PATH, "data")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize(
|
|
15
|
+
"minx, maxx, miny, maxy, expected_origin",
|
|
16
|
+
[
|
|
17
|
+
(501, 999, 501, 999, (0, 1000)), # points in the second half
|
|
18
|
+
(1, 400, 1, 400, (0, 1000)), # points in the first half
|
|
19
|
+
(500, 1000, 500, 500, (0, 1000)), # xmax on edge and xmin in the tile
|
|
20
|
+
(0, 20, 500, 500, (0, 1000)), # xmin on edge and xmax in the tile
|
|
21
|
+
(950, 1000, 500, 500, (0, 1000)), # xmax on edge and xmin in the tile
|
|
22
|
+
(500, 500, 980, 1000, (0, 1000)), # ymax on edge and ymin in the tile
|
|
23
|
+
(500, 500, 0, 20, (0, 1000)), # ymin on edge and ymax in the tile
|
|
24
|
+
(0, 1000, 0, 1000, (0, 1000)), # points at each corner
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
def test_infer_tile_origin_edge_cases(minx, maxx, miny, maxy, expected_origin):
|
|
28
|
+
origin_x, origin_y = pcd_info.infer_tile_origin(minx, maxx, miny, maxy, tile_width=1000)
|
|
29
|
+
assert (origin_x, origin_y) == expected_origin
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize(
|
|
33
|
+
"minx, maxx, miny, maxy",
|
|
34
|
+
[
|
|
35
|
+
(0, 20, -1, 20), # ymin slightly outside the tile
|
|
36
|
+
(-1, 20, 0, 20), # xmin slightly outside the tile
|
|
37
|
+
(280, 1000, 980, 1001), # ymax slightly outside the tile
|
|
38
|
+
(980, 1001, 980, 1000), # xmax slightly outside the tile
|
|
39
|
+
(-1, 1000, 0, 1000), # xmax on edge but xmin outside the tile
|
|
40
|
+
(0, 1000, 0, 1001), # ymin on edge but ymax outside the tile
|
|
41
|
+
(0, 1001, 0, 1000), # xmin on edge but xmax outside the tile
|
|
42
|
+
(0, 1000, -1, 1000), # ymax on edge but ymin outside the tile
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
def test_infer_tile_origin_edge_cases_fail(minx, maxx, miny, maxy):
|
|
46
|
+
with pytest.raises(ValueError):
|
|
47
|
+
pcd_info.infer_tile_origin(minx, maxx, miny, maxy, tile_width=1000)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize(
|
|
51
|
+
"input_points",
|
|
52
|
+
[
|
|
53
|
+
(np.array([[0, -1, 0], [20, 20, 0]])), # ymin slightly outside the tile
|
|
54
|
+
(np.array([[-1, 0, 0], [20, 20, 0]])), # xmin slightly outside the tile
|
|
55
|
+
(np.array([[980, 980, 0], [1000, 1001, 0]])), # ymax slightly outside the tile
|
|
56
|
+
(np.array([[980, 980, 0], [1001, 1000, 0]])), # xmax slightly outside the tile
|
|
57
|
+
(np.array([[-1, 0, 0], [1000, 1000, 0]])), # xmax on edge but xmin outside the tile
|
|
58
|
+
(np.array([[0, 0, 0], [1000, 1001, 0]])), # ymin on edge but ymax outside the tile
|
|
59
|
+
(np.array([[0, 0, 0], [1001, 1000, 0]])), # xmin on edge but xmax outside the tile
|
|
60
|
+
(np.array([[0, -1, 0], [1000, 1000, 0]])), # ymax on edge but ymin outside the tile
|
|
61
|
+
],
|
|
62
|
+
)
|
|
63
|
+
def test_get_pointcloud_origin_edge_cases_fail(input_points):
|
|
64
|
+
with pytest.raises(ValueError):
|
|
65
|
+
pcd_info.get_pointcloud_origin_from_tile_width(points=input_points, tile_width=1000)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_get_pointcloud_origin_on_file():
|
|
69
|
+
input_las = os.path.join(DATA_PATH, "test_data_77055_627760_LA93_IGN69.laz")
|
|
70
|
+
expected_origin = (770550, 6277600)
|
|
71
|
+
LAS = laspy.read(input_las)
|
|
72
|
+
INPUT_POINTS = np.vstack((LAS.x, LAS.y, LAS.z)).transpose()
|
|
73
|
+
|
|
74
|
+
origin_x, origin_y = pcd_info.get_pointcloud_origin_from_tile_width(points=INPUT_POINTS, tile_width=50)
|
|
75
|
+
assert (origin_x, origin_y) == expected_origin
|
|
76
|
+
origin_x_2, origin_y_2 = pcd_info.get_pointcloud_origin_from_tile_width(
|
|
77
|
+
points=INPUT_POINTS, tile_width=10, buffer_size=20
|
|
78
|
+
)
|
|
79
|
+
assert (origin_x_2, origin_y_2) == (expected_origin[0] + 20, expected_origin[1] - 20)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_get_pointcloud_origin_fail_on_buffersize():
|
|
83
|
+
with pytest.raises(ValueError):
|
|
84
|
+
# Case when buffer size is bigger than the tile extremities (case not handled)
|
|
85
|
+
points = np.array([[0, 0, 0], [20, 20, 0]])
|
|
86
|
+
buffer_size = 30
|
|
87
|
+
pcd_info.get_pointcloud_origin_from_tile_width(points=points, tile_width=1000, buffer_size=buffer_size)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import platform
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess as sp
|
|
5
6
|
from test.utils import EXPECTED_DIMS_BY_DATAFORMAT, get_pdal_infos_summary
|
|
@@ -7,14 +8,19 @@ from test.utils import EXPECTED_DIMS_BY_DATAFORMAT, get_pdal_infos_summary
|
|
|
7
8
|
import pdal
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
11
|
+
from pdaltools.count_occurences.count_occurences_for_attribute import (
|
|
12
|
+
compute_count_one_file,
|
|
13
|
+
)
|
|
10
14
|
from pdaltools.standardize_format import exec_las2las, rewrite_with_pdal, standardize
|
|
11
15
|
|
|
12
16
|
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
13
17
|
TMP_PATH = os.path.join(TEST_PATH, "tmp")
|
|
14
18
|
INPUT_DIR = os.path.join(TEST_PATH, "data")
|
|
15
19
|
|
|
20
|
+
DEFAULT_PARAMS = {"dataformat_id": 6, "a_srs": "EPSG:2154", "extra_dims": []}
|
|
21
|
+
|
|
16
22
|
MUTLIPLE_PARAMS = [
|
|
17
|
-
|
|
23
|
+
DEFAULT_PARAMS,
|
|
18
24
|
{"dataformat_id": 8, "a_srs": "EPSG:4326", "extra_dims": []},
|
|
19
25
|
{"dataformat_id": 8, "a_srs": "EPSG:2154", "extra_dims": ["dtm_marker=double", "dsm_marker=double"]},
|
|
20
26
|
{"dataformat_id": 8, "a_srs": "EPSG:2154", "extra_dims": "all"},
|
|
@@ -30,8 +36,19 @@ def setup_module(module):
|
|
|
30
36
|
os.mkdir(TMP_PATH)
|
|
31
37
|
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
@pytest.mark.parametrize(
|
|
40
|
+
"params",
|
|
41
|
+
[
|
|
42
|
+
DEFAULT_PARAMS,
|
|
43
|
+
{"dataformat_id": 8, "a_srs": "EPSG:4326", "extra_dims": []},
|
|
44
|
+
{"dataformat_id": 8, "a_srs": "EPSG:2154", "extra_dims": ["dtm_marker=double", "dsm_marker=double"]},
|
|
45
|
+
{"dataformat_id": 8, "a_srs": "EPSG:2154", "extra_dims": "all"},
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
def test_standardize_format(params):
|
|
49
|
+
input_file = os.path.join(INPUT_DIR, "test_data_77055_627755_LA93_IGN69_extra_dims.laz")
|
|
50
|
+
output_file = os.path.join(TMP_PATH, "formatted.laz")
|
|
51
|
+
rewrite_with_pdal(input_file, output_file, params, [])
|
|
35
52
|
# check file exists
|
|
36
53
|
assert os.path.isfile(output_file)
|
|
37
54
|
# check values from metadata
|
|
@@ -54,19 +71,43 @@ def _test_standardize_format_one_params_set(input_file, output_file, params):
|
|
|
54
71
|
extra_dims_names = [dim.split("=")[0] for dim in params["extra_dims"]]
|
|
55
72
|
assert dimensions == EXPECTED_DIMS_BY_DATAFORMAT[params["dataformat_id"]].union(extra_dims_names)
|
|
56
73
|
|
|
74
|
+
# Check that there is the expected number of points for each class
|
|
75
|
+
expected_points_counts = compute_count_one_file(input_file)
|
|
76
|
+
|
|
77
|
+
output_points_counts = compute_count_one_file(output_file)
|
|
78
|
+
assert output_points_counts == expected_points_counts
|
|
79
|
+
|
|
57
80
|
# TODO: Check srs
|
|
58
81
|
# TODO: check precision
|
|
59
82
|
|
|
60
83
|
|
|
61
|
-
|
|
84
|
+
@pytest.mark.parametrize(
|
|
85
|
+
"classes_to_remove",
|
|
86
|
+
[
|
|
87
|
+
[],
|
|
88
|
+
[2, 3],
|
|
89
|
+
[1, 2, 3, 4, 5, 6, 64], # remove all classes
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
def test_standardize_classes(classes_to_remove):
|
|
62
93
|
input_file = os.path.join(INPUT_DIR, "test_data_77055_627755_LA93_IGN69_extra_dims.laz")
|
|
63
94
|
output_file = os.path.join(TMP_PATH, "formatted.laz")
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
rewrite_with_pdal(input_file, output_file, DEFAULT_PARAMS, classes_to_remove)
|
|
96
|
+
# Check that there is the expected number of points for each class
|
|
97
|
+
expected_points_counts = compute_count_one_file(input_file)
|
|
98
|
+
for cl in classes_to_remove:
|
|
99
|
+
expected_points_counts.pop(str(cl))
|
|
100
|
+
|
|
101
|
+
output_points_counts = compute_count_one_file(output_file)
|
|
102
|
+
assert output_points_counts == expected_points_counts
|
|
66
103
|
|
|
67
104
|
|
|
68
105
|
def exec_lasinfo(input_file: str):
|
|
69
|
-
|
|
106
|
+
if platform.processor() == "arm" and platform.architecture()[0] == "64bit":
|
|
107
|
+
lasinfo = "lasinfo64"
|
|
108
|
+
else:
|
|
109
|
+
lasinfo = "lasinfo"
|
|
110
|
+
r = sp.run([lasinfo, "-stdout", input_file], stderr=sp.PIPE, stdout=sp.PIPE)
|
|
70
111
|
if r.returncode == 1:
|
|
71
112
|
msg = r.stderr.decode()
|
|
72
113
|
print(msg)
|
|
@@ -102,14 +143,14 @@ def test_standardize_does_NOT_produce_any_warning_with_Lasinfo():
|
|
|
102
143
|
# if you want to see input_file warnings
|
|
103
144
|
# assert_lasinfo_no_warning(input_file)
|
|
104
145
|
|
|
105
|
-
standardize(input_file, output_file,
|
|
146
|
+
standardize(input_file, output_file, DEFAULT_PARAMS, [])
|
|
106
147
|
assert_lasinfo_no_warning(output_file)
|
|
107
148
|
|
|
108
149
|
|
|
109
150
|
def test_standardize_malformed_laz():
|
|
110
151
|
input_file = os.path.join(TEST_PATH, "data/test_pdalfail_0643_6319_LA93_IGN69.laz")
|
|
111
152
|
output_file = os.path.join(TMP_PATH, "standardize_pdalfail_0643_6319_LA93_IGN69.laz")
|
|
112
|
-
standardize(input_file, output_file,
|
|
153
|
+
standardize(input_file, output_file, DEFAULT_PARAMS, [])
|
|
113
154
|
assert os.path.isfile(output_file)
|
|
114
155
|
|
|
115
156
|
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"""Tools to get information from a point cloud (points as a numpy array)"""
|
|
2
|
-
|
|
3
|
-
from typing import Tuple
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def get_pointcloud_origin_from_tile_width(
|
|
9
|
-
points: np.ndarray, tile_width: int = 1000, buffer_size: float = 0
|
|
10
|
-
) -> Tuple[int, int]:
|
|
11
|
-
"""Get point cloud theoretical origin (xmin, ymax) for a data that originates from a square tesselation/tiling
|
|
12
|
-
using the tesselation tile width only.
|
|
13
|
-
|
|
14
|
-
Edge values are supposed to be included in the tile
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
points (np.ndarray): numpy array with the tile points
|
|
19
|
-
tile_width (int, optional): Edge size of the square used for tiling. Defaults to 1000.
|
|
20
|
-
buffer_size (float, optional): Optional buffer around the tile. Defaults to 0.
|
|
21
|
-
|
|
22
|
-
Raises:
|
|
23
|
-
ValueError: Raise an error when the bounding box of the tile is not included in a tile
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
Tuple[int, int]: (origin_x, origin_y) origin coordinates
|
|
27
|
-
"""
|
|
28
|
-
# Extract coordinates xmin, xmax, ymin and ymax of the original tile without buffer
|
|
29
|
-
x_min, y_min = np.min(points[:, :2], axis=0) + buffer_size
|
|
30
|
-
x_max, y_max = np.max(points[:, :2], axis=0) - buffer_size
|
|
31
|
-
|
|
32
|
-
# Calculate the tiles to which x, y bounds belong
|
|
33
|
-
tile_x_min = np.floor(x_min / tile_width)
|
|
34
|
-
tile_x_max = np.floor(x_max / tile_width) if x_max % tile_width != 0 else np.floor(x_max / tile_width) - 1
|
|
35
|
-
tile_y_min = np.ceil(y_min / tile_width) if y_min % tile_width != 0 else np.floor(y_min / tile_width) + 1
|
|
36
|
-
tile_y_max = np.ceil(y_max / tile_width)
|
|
37
|
-
|
|
38
|
-
if not (tile_x_max - tile_x_min) and not (tile_y_max - tile_y_min):
|
|
39
|
-
origin_x = tile_x_min * tile_width
|
|
40
|
-
origin_y = tile_y_max * tile_width
|
|
41
|
-
return origin_x, origin_y
|
|
42
|
-
else:
|
|
43
|
-
raise ValueError(
|
|
44
|
-
f"Min values (x={x_min} and y={y_min}) do not belong to the same theoretical tile as"
|
|
45
|
-
f"max values (x={x_max} and y={y_max})."
|
|
46
|
-
)
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
import laspy
|
|
4
|
-
import numpy as np
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from pdaltools import pcd_info
|
|
8
|
-
|
|
9
|
-
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
10
|
-
TMP_PATH = os.path.join(TEST_PATH, "tmp")
|
|
11
|
-
DATA_PATH = os.path.join(TEST_PATH, "data")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@pytest.mark.parametrize(
|
|
15
|
-
"input_points, expected_origin",
|
|
16
|
-
[
|
|
17
|
-
(np.array([[501, 501, 0], [999, 999, 0]]), (0, 1000)), # points in the second half
|
|
18
|
-
(np.array([[1, 1, 0], [400, 400, 0]]), (0, 1000)), # points in the frist half
|
|
19
|
-
(np.array([[500, 500, 0], [1000, 500, 0]]), (0, 1000)), # xmax on edge and xmin in the tile
|
|
20
|
-
(np.array([[0, 500, 0], [20, 500, 0]]), (0, 1000)), # xmin on edge and xmax in the tile
|
|
21
|
-
(np.array([[950, 500, 0], [1000, 500, 0]]), (0, 1000)), # xmax on edge and xmin in the tile
|
|
22
|
-
(np.array([[500, 980, 0], [500, 1000, 0]]), (0, 1000)), # ymax on edge and ymin in the tile
|
|
23
|
-
(np.array([[500, 0, 0], [500, 20, 0]]), (0, 1000)), # ymin on edge and ymax in the tile
|
|
24
|
-
(np.array([[0, 0, 0], [1000, 1000, 0]]), (0, 1000)), # points at each corner
|
|
25
|
-
],
|
|
26
|
-
)
|
|
27
|
-
def test_get_pointcloud_origin_edge_cases(input_points, expected_origin):
|
|
28
|
-
origin_x, origin_y = pcd_info.get_pointcloud_origin_from_tile_width(points=input_points, tile_width=1000)
|
|
29
|
-
assert (origin_x, origin_y) == expected_origin
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.mark.parametrize(
|
|
33
|
-
"input_points",
|
|
34
|
-
[
|
|
35
|
-
(np.array([[0, -1, 0], [20, 20, 0]])), # ymin slightly outside the tile
|
|
36
|
-
(np.array([[-1, 0, 0], [20, 20, 0]])), # xmin slightly outside the tile
|
|
37
|
-
(np.array([[980, 980, 0], [1000, 1001, 0]])), # ymax slightly outside the tile
|
|
38
|
-
(np.array([[980, 980, 0], [1001, 1000, 0]])), # xmax slightly outside the tile
|
|
39
|
-
(np.array([[-1, 0, 0], [1000, 1000, 0]])), # xmax on edge but xmin outside the tile
|
|
40
|
-
(np.array([[0, 0, 0], [1000, 1001, 0]])), # ymin on edge but ymax outside the tile
|
|
41
|
-
(np.array([[0, 0, 0], [1001, 1000, 0]])), # xmin on edge but xmax outside the tile
|
|
42
|
-
(np.array([[0, -1, 0], [1000, 1000, 0]])), # ymax on edge but ymin outside the tile
|
|
43
|
-
],
|
|
44
|
-
)
|
|
45
|
-
def test_get_pointcloud_origin_edge_cases_fail(input_points):
|
|
46
|
-
with pytest.raises(ValueError):
|
|
47
|
-
pcd_info.get_pointcloud_origin_from_tile_width(points=input_points, tile_width=1000)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_get_pointcloud_origin_on_file():
|
|
51
|
-
input_las = os.path.join(DATA_PATH, "test_data_77055_627760_LA93_IGN69.laz")
|
|
52
|
-
expected_origin = (770550, 6277600)
|
|
53
|
-
LAS = laspy.read(input_las)
|
|
54
|
-
INPUT_POINTS = np.vstack((LAS.x, LAS.y, LAS.z)).transpose()
|
|
55
|
-
|
|
56
|
-
origin_x, origin_y = pcd_info.get_pointcloud_origin_from_tile_width(points=INPUT_POINTS, tile_width=50)
|
|
57
|
-
assert (origin_x, origin_y) == expected_origin
|
|
58
|
-
origin_x_2, origin_y_2 = pcd_info.get_pointcloud_origin_from_tile_width(
|
|
59
|
-
points=INPUT_POINTS, tile_width=10, buffer_size=20
|
|
60
|
-
)
|
|
61
|
-
assert (origin_x_2, origin_y_2) == (expected_origin[0] + 20, expected_origin[1] - 20)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|