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.
Files changed (34) hide show
  1. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/PKG-INFO +6 -1
  2. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/README.md +5 -0
  3. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/PKG-INFO +6 -1
  4. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/SOURCES.txt +2 -0
  5. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/_version.py +1 -1
  6. ign_pdal_tools-1.7.5/pdaltools/add_points_in_las.py +104 -0
  7. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/color.py +13 -9
  8. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_info.py +27 -1
  9. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_remove_dimensions.py +2 -2
  10. ign_pdal_tools-1.7.5/pdaltools/pcd_info.py +76 -0
  11. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/standardize_format.py +31 -11
  12. ign_pdal_tools-1.7.5/test/test_add_points_in_las.py +72 -0
  13. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_info.py +5 -0
  14. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_remove_dimensions.py +11 -6
  15. ign_pdal_tools-1.7.5/test/test_pcd_info.py +87 -0
  16. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_standardize_format.py +50 -9
  17. ign_pdal_tools-1.7.3/pdaltools/pcd_info.py +0 -46
  18. ign_pdal_tools-1.7.3/test/test_pcd_info.py +0 -61
  19. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/LICENSE.md +0 -0
  20. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/dependency_links.txt +0 -0
  21. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/ign_pdal_tools.egg-info/top_level.txt +0 -0
  22. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_add_buffer.py +0 -0
  23. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_clip.py +0 -0
  24. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/las_merge.py +0 -0
  25. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/replace_attribute_in_las.py +0 -0
  26. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pdaltools/unlock_file.py +0 -0
  27. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/pyproject.toml +0 -0
  28. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/setup.cfg +0 -0
  29. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_color.py +0 -0
  30. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_add_buffer.py +0 -0
  31. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_clip.py +0 -0
  32. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_las_merge.py +0 -0
  33. {ign_pdal_tools-1.7.3 → ign_pdal_tools-1.7.5}/test/test_replace_attribute_in_las.py +0 -0
  34. {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
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
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
@@ -1,4 +1,4 @@
1
- __version__ = "1.7.3"
1
+ __version__ = "1.7.5"
2
2
 
3
3
 
4
4
  if __name__ == "__main__":
@@ -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
- # Give single-point clouds a width/height of at least one pixel to have valid BBOX and SIZE
73
- if minx == maxx:
74
- maxx = minx + 1 / pixel_per_meter
75
- if miny == maxy:
76
- maxy = miny + 1 / pixel_per_meter
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
- "dimensions of las"
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(input_file: str, output_file: str, params_from_parser: Dict) -> None:
69
- # Update parameters with command line values
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.Reader.las(input_file)
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
- r = sp.run(["las2las", "-i", input_file, "-o", output_file], stderr=sp.PIPE, stdout=sp.PIPE)
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(dataformat_id=args.record_format, a_srs=args.projection, extra_dims=args.extra_dims)
99
- standardize(args.input_file, args.output_file, params_from_parser)
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
- def get_points(input_las : str):
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
- def append_dimension(input_las : str, output_las : str):
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(output_las, extra_dims="all", forward="all", )
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
- with pytest.raises(ValueError):
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
- {"dataformat_id": 6, "a_srs": "EPSG:2154", "extra_dims": []},
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
- def _test_standardize_format_one_params_set(input_file, output_file, params):
34
- rewrite_with_pdal(input_file, output_file, params)
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
- def test_standardize_format():
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
- for params in MUTLIPLE_PARAMS:
65
- _test_standardize_format_one_params_set(input_file, output_file, params)
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
- r = sp.run(["lasinfo", "-stdout", input_file], stderr=sp.PIPE, stdout=sp.PIPE)
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, MUTLIPLE_PARAMS[0])
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, MUTLIPLE_PARAMS[0])
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