ign-pdal-tools 1.12.0__py3-none-any.whl → 1.12.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ign-pdal-tools
3
- Version: 1.12.0
3
+ Version: 1.12.2
4
4
  Summary: Library for common LAS files manipulation with PDAL
5
5
  Author-email: Guillaume Liegard <guillaume.liegard@ign.fr>
6
6
  Description-Content-Type: text/markdown
@@ -1,20 +1,20 @@
1
- ign_pdal_tools-1.12.0.dist-info/licenses/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
2
- pdaltools/_version.py,sha256=uuG85nhWU5wpaYiHuTFYsaS18txZLNqRwMOK8z2G96k,75
1
+ ign_pdal_tools-1.12.2.dist-info/licenses/LICENSE.md,sha256=iVzCFZTUXeiqP8bP474iuWZiWO_kDCD4SPh1Wiw125Y,1120
2
+ pdaltools/_version.py,sha256=hNkZafWzNyaa6UC2MRErLwCS5CjHrj7c2pqE_mjcoug,75
3
3
  pdaltools/add_points_in_pointcloud.py,sha256=6NclQeAFYyVz3kfJ114BEFKfM5nwWWC2c8iN4IpaPOc,12662
4
4
  pdaltools/color.py,sha256=vJgpb8dOvT5rnq5NdVOaMdGc_pKL3damLy4HwGvigJQ,14472
5
- pdaltools/create_random_laz.py,sha256=RxRzMGZ33xoomu4eh-cLSj4tj5Gy41rBzk08lAKHHzg,5726
5
+ pdaltools/create_random_laz.py,sha256=kFe5iHeHlkgKWRIKjK5l1AD65OG4qLYwAZdO1Wcvuos,5255
6
6
  pdaltools/las_add_buffer.py,sha256=rnFExAfi0KqlQpL4hDMh2aC08AcYdSHSB6WPG5RyFIc,11274
7
7
  pdaltools/las_clip.py,sha256=GvEOYu8RXV68e35kU8i42GwSkbo4P9TvmS6rkrdPmFM,1034
8
- pdaltools/las_comparison.py,sha256=pq0fa_kOkysPBBYNNcGg9FFyRLT6IANdPjaGwQALwVU,4193
8
+ pdaltools/las_comparison.py,sha256=B9hFGbmD0x4JEN4oHbiQFNbd0T-9P3mnAN67Czu0pZk,4505
9
9
  pdaltools/las_info.py,sha256=lMKxKzsViptDENI1wOlANG4qOvdc19ixyasYKD-N1ck,9512
10
10
  pdaltools/las_merge.py,sha256=tcFVueV9X9nNEaoAl5zCduY5DETlBg63MAgP2SuKiNo,4121
11
11
  pdaltools/las_remove_dimensions.py,sha256=f8imGhN6LNTuQ1GMJQRzIIV3Wab_oRPOyEnKi1CgfiM,2318
12
12
  pdaltools/las_rename_dimension.py,sha256=zXEKHyx1uQ3U0oZYo_BTnqbTHGSq5TIZHqZn_EPqNKQ,2576
13
13
  pdaltools/pcd_info.py,sha256=NIAH5KGikVDQLlbCcw9FuaPqe20UZvRfkHsDZd5kmZA,3210
14
- pdaltools/replace_attribute_in_las.py,sha256=po1F-fi8s7iilqKWaryW4JRbsmdMOUe0yGvG3AEKxtk,4771
15
- pdaltools/standardize_format.py,sha256=Z09yhY_dRaX0uNO0K_Ml5ZD3XpVDv4Q2gIyZHXaplAQ,4849
14
+ pdaltools/replace_attribute_in_las.py,sha256=MHpIizSupgWtbizteoRH8FKDE049hrAh4v_OhmRmSPU,4318
15
+ pdaltools/standardize_format.py,sha256=I2oNiwhSMtr4e5ZK9qbB_yKmy3twOoO6QLiSFu4_AaI,3905
16
16
  pdaltools/unlock_file.py,sha256=G2odk0cpp_X9r49Y90oK88v3qlihaMfg6acwmWqblik,1958
17
- ign_pdal_tools-1.12.0.dist-info/METADATA,sha256=UAKzHKeQ_jlZB_uwlRWWfeV2vJO1HVEXvIfMu7om554,5778
18
- ign_pdal_tools-1.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- ign_pdal_tools-1.12.0.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
20
- ign_pdal_tools-1.12.0.dist-info/RECORD,,
17
+ ign_pdal_tools-1.12.2.dist-info/METADATA,sha256=cOFyIrusuyvAsB_nrIRUaV0Y9h-Nle8ou8FU3POAoAA,5778
18
+ ign_pdal_tools-1.12.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ ign_pdal_tools-1.12.2.dist-info/top_level.txt,sha256=KvGW0ZzqQbhCKzB5_Tp_buWMZyIgiO2M2krWF_ecOZc,10
20
+ ign_pdal_tools-1.12.2.dist-info/RECORD,,
pdaltools/_version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.12.0"
1
+ __version__ = "1.12.2"
2
2
 
3
3
 
4
4
  if __name__ == "__main__":
@@ -1,78 +1,82 @@
1
1
  import numpy as np
2
2
  import laspy
3
3
  from pathlib import Path
4
- import sys
5
4
  import argparse
6
- import pdal
7
5
  from pyproj import CRS
8
- from typing import List, Tuple, Union
6
+ from typing import List, Tuple
9
7
 
10
- def create_random_laz(output_file: str, point_format: int = 3, num_points: int = 100, crs: int = 2154,
11
- center: Tuple[float, float] = (650000, 6810000),
12
- extra_dims: List[Tuple[str, str]] = [],
13
- ):
8
+
9
+ def create_random_laz(
10
+ output_file: str,
11
+ point_format: int = 3,
12
+ num_points: int = 100,
13
+ crs: int = 2154,
14
+ center: Tuple[float, float] = (650000, 6810000),
15
+ extra_dims: List[Tuple[str, str]] = [],
16
+ ):
14
17
  """
15
18
  Create a test LAZ file with EPSG code and additional dimensions.
16
-
19
+
17
20
  Args:
18
21
  output_file: Path to save the LAZ file
19
22
  point_format: Point format of the LAZ file (default: 3)
20
23
  num_points: Number of points to generate
21
24
  crs: EPSG code of the CRS (default: 2154)
22
- center: Tuple of floats (x, y) of the center of the area to generate points in (default: (650000, 6810000) ; around Paris)
25
+ center: Tuple of floats (x, y) of the center of the area to generate points in
26
+ (default: (650000, 6810000) ; around Paris)
23
27
  extra_dims: List of tuples (dimension_name, dimension_type) where type can be:
24
28
  'float32', 'float64', 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'
25
29
  """
26
-
30
+
27
31
  # Create a new point cloud
28
32
  header = laspy.LasHeader(point_format=point_format, version="1.4")
29
-
33
+
30
34
  # Map string types to numpy types
31
35
  type_mapping = {
32
- 'float32': np.float32,
33
- 'float64': np.float64,
34
- 'int8': np.int8,
35
- 'int16': np.int16,
36
- 'int32': np.int32,
37
- 'int64': np.int64,
38
- 'uint8': np.uint8,
39
- 'uint16': np.uint16,
40
- 'uint32': np.uint32,
41
- 'uint64': np.uint64,
36
+ "float32": np.float32,
37
+ "float64": np.float64,
38
+ "int8": np.int8,
39
+ "int16": np.int16,
40
+ "int32": np.int32,
41
+ "int64": np.int64,
42
+ "uint8": np.uint8,
43
+ "uint16": np.uint16,
44
+ "uint32": np.uint32,
45
+ "uint64": np.uint64,
42
46
  }
43
-
47
+
44
48
  for dim_name, dim_type in extra_dims:
45
49
  if dim_type not in type_mapping:
46
50
  raise ValueError(f"Unsupported dimension type: {dim_type}. Supported types: {list(type_mapping.keys())}")
47
-
51
+
48
52
  numpy_type = type_mapping[dim_type]
49
53
  header.add_extra_dim(laspy.ExtraBytesParams(name=dim_name, type=numpy_type))
50
-
54
+
51
55
  # Create point cloud
52
56
  las = laspy.LasData(header)
53
57
  las.header.add_crs(CRS.from_string(f"epsg:{crs}"))
54
-
58
+
55
59
  # Generate random points in a small area
56
60
  las.x = np.random.uniform(center[0] - 1000, center[0] + 1000, num_points)
57
61
  las.y = np.random.uniform(center[1] - 1000, center[1] + 1000, num_points)
58
62
  las.z = np.random.uniform(0, 200, num_points)
59
-
63
+
60
64
  # Generate random intensity values
61
65
  las.intensity = np.random.randint(0, 255, num_points)
62
-
66
+
63
67
  # Generate random classification values
64
- # 66 is the max value for classification of IGN LidarHD
68
+ # 66 is the max value for classification of IGN LidarHD
65
69
  # cf. https://geoservices.ign.fr/sites/default/files/2022-05/DT_LiDAR_HD_1-0.pdf
66
- if point_format > 3:
70
+ if point_format > 3:
67
71
  num_classifications = 66
68
72
  else:
69
73
  num_classifications = 10
70
74
  las.classification = np.random.randint(0, num_classifications, num_points)
71
-
75
+
72
76
  # Generate random values for each extra dimension
73
77
  for dim_name, dim_type in extra_dims:
74
78
  numpy_type = type_mapping[dim_type]
75
-
79
+
76
80
  # Generate appropriate random values based on the type
77
81
  if numpy_type in [np.float32, np.float64]:
78
82
  las[dim_name] = np.random.uniform(0, 10, num_points).astype(numpy_type)
@@ -80,7 +84,7 @@ def create_random_laz(output_file: str, point_format: int = 3, num_points: int =
80
84
  las[dim_name] = np.random.randint(-100, 100, num_points).astype(numpy_type)
81
85
  elif numpy_type in [np.uint8, np.uint16, np.uint32, np.uint64]:
82
86
  las[dim_name] = np.random.randint(0, 100, num_points).astype(numpy_type)
83
-
87
+
84
88
  # Write to file
85
89
  las.write(output_file)
86
90
  dimensions = list(las.point_format.dimension_names)
@@ -91,7 +95,7 @@ def create_random_laz(output_file: str, point_format: int = 3, num_points: int =
91
95
  }
92
96
 
93
97
 
94
- def test_output_file(result : dict, output_file: str):
98
+ def test_output_file(result: dict, output_file: str):
95
99
 
96
100
  # Validate output file path
97
101
  output_path = Path(output_file)
@@ -103,15 +107,6 @@ def test_output_file(result : dict, output_file: str):
103
107
  print(f"Number of points: {result['num_points']}")
104
108
  print(f"Dimensions available: {result['dimensions']}")
105
109
 
106
- # Print available dimensions using PDAL
107
- pipeline = pdal.Pipeline() | pdal.Reader.las(result['output_file'])
108
- pipeline.execute()
109
- points = pipeline.arrays[0]
110
- dimensions = list(points.dtype.fields.keys())
111
- print("\nAvailable dimensions in input file:")
112
- for dim in dimensions:
113
- print(f"- {dim}")
114
-
115
110
 
116
111
  def parse_args():
117
112
  # Parse arguments (assuming argparse is used)
@@ -119,28 +114,33 @@ def parse_args():
119
114
  parser.add_argument("--output_file", type=str, help="Path to save the LAZ file")
120
115
  parser.add_argument("--point_format", type=int, default=3, help="Point format of the LAZ file")
121
116
  parser.add_argument("--num_points", type=int, default=100, help="Number of points to generate")
122
- parser.add_argument("--extra_dims", type=str, nargs="*", default=[], help="Extra dimensions in the format name:type")
117
+ parser.add_argument(
118
+ "--extra_dims", type=str, nargs="*", default=[], help="Extra dimensions in the format name:type"
119
+ )
123
120
  parser.add_argument("--crs", type=int, default=2154, help="Projection code")
124
- parser.add_argument("--center", type=str, default="650000,6810000", help="Center of the area to generate points in")
121
+ parser.add_argument(
122
+ "--center", type=str, default="650000,6810000", help="Center of the area to generate points in"
123
+ )
125
124
  return parser.parse_args()
126
125
 
127
126
 
128
127
  def main():
129
-
128
+
130
129
  # Parse arguments
131
130
  args = parse_args()
132
-
131
+
133
132
  # Parse extra dimensions
134
133
  extra_dims = [tuple(dim.split(":")) for dim in args.extra_dims]
135
-
134
+
136
135
  # Parse center
137
136
  center = tuple(map(float, args.center.split(",")))
138
137
 
139
138
  # Call create_random_laz
140
139
  result = create_random_laz(args.output_file, args.point_format, args.num_points, args.crs, center, extra_dims)
141
-
140
+
142
141
  # Test output file
143
- test_output_file(result, args.output_file)
142
+ test_output_file(result, args.output_file)
143
+
144
144
 
145
145
  if __name__ == "__main__":
146
146
  main()
@@ -2,50 +2,58 @@ import laspy
2
2
  from pathlib import Path
3
3
  import numpy as np
4
4
  import argparse
5
+ from typing import Tuple
5
6
 
6
- def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) -> bool:
7
+
8
+ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) -> Tuple[bool, int, float]:
7
9
  """
8
10
  Compare specified dimensions between two LAS files.
9
11
  If no dimensions are specified, compares all available dimensions.
10
12
  Sorts points by x,y,z,gps_time coordinates before comparison to ensure point order consistency.
11
-
13
+
12
14
  Args:
13
15
  file1: Path to the first LAS file
14
16
  file2: Path to the second LAS file
15
17
  dimensions: List of dimension names to compare (optional)
16
-
18
+
17
19
  Returns:
18
20
  bool: True if all specified dimensions are identical, False otherwise
21
+ int: Number of points with different dimensions
22
+ float: Percentage of points with different dimensions
19
23
  """
20
24
  try:
21
25
  # Read both LAS files
22
26
  las1 = laspy.read(file1)
23
27
  las2 = laspy.read(file2)
24
-
28
+
25
29
  # Check if files have the same number of points
26
30
  if len(las1) != len(las2):
27
31
  print(f"Files have different number of points: {len(las1)} vs {len(las2)}")
28
- return False
29
-
32
+ return False, 0, 0
33
+ print(f"Files have the same number of points: {len(las1)} vs {len(las2)}")
34
+
30
35
  # Sort points by x,y,z,gps_time coordinates
31
36
  # Create sorting indices
32
37
  sort_idx1 = np.lexsort((las1.z, las1.y, las1.x, las1.gps_time))
33
38
  sort_idx2 = np.lexsort((las2.z, las2.y, las2.x, las2.gps_time))
34
-
39
+
35
40
  # If no dimensions specified, compare all dimensions
36
41
  dimensions_las1 = sorted(las1.point_format.dimension_names)
37
42
  dimensions_las2 = sorted(las2.point_format.dimension_names)
38
-
43
+
39
44
  if dimensions is None:
40
45
  if dimensions_las1 != dimensions_las2:
41
46
  print("Files have different dimensions")
42
- return False
43
- dimensions = dimensions_las1
47
+ return False, 0, 0
48
+ dimensions = dimensions_las1
44
49
  else:
45
50
  for dim in dimensions:
46
51
  if dim not in dimensions_las1 or dim not in dimensions_las2:
47
- print(f"Dimension '{dim}' is not found in one or both files. Available dimensions: {las1.point_format.dimension_names}")
48
- return False
52
+ print(
53
+ f"Dimension '{dim}' is not found in one or both files.\n"
54
+ f"Available dimensions: {las1.point_format.dimension_names}"
55
+ )
56
+ return False, 0, 0
49
57
 
50
58
  # Compare each dimension
51
59
  for dim in dimensions:
@@ -53,7 +61,7 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
53
61
  # Get sorted dimension arrays
54
62
  dim1 = np.array(las1[dim])[sort_idx1]
55
63
  dim2 = np.array(las2[dim])[sort_idx2]
56
-
64
+
57
65
  # Compare dimensions
58
66
  if not np.array_equal(dim1, dim2):
59
67
  # Find differences
@@ -63,43 +71,47 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
63
71
  print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}")
64
72
  if len(diff_indices) > 10:
65
73
  print(f"... and {len(diff_indices) - 10} more differences")
66
- return False
67
-
74
+ return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
75
+
68
76
  except KeyError:
69
77
  print(f"Dimension '{dim}' not found in one or both files")
70
- return False
71
-
72
- return True
73
-
78
+ return False, 0, 0
79
+
80
+ return True, 0, 0
81
+
74
82
  except laspy.errors.LaspyException as e:
75
83
  print(f"LAS file error: {str(e)}")
76
- return False
84
+ return False, 0, 0
77
85
  except FileNotFoundError as e:
78
86
  print(f"File not found: {str(e)}")
79
- return False
87
+ return False, 0, 0
80
88
  except ValueError as e:
81
89
  print(f"Value error: {str(e)}")
82
- return False
90
+ return False, 0, 0
91
+
83
92
 
84
93
  # Update main function to use the new compare function
85
- def main():
86
- parser = argparse.ArgumentParser(description='Compare dimensions between two LAS files')
87
- parser.add_argument('file1', type=str, help='Path to first LAS file')
88
- parser.add_argument('file2', type=str, help='Path to second LAS file')
89
- parser.add_argument('--dimensions', nargs='*', help='List of dimensions to compare. If not specified, compares all dimensions.')
90
-
94
+ def main():
95
+ parser = argparse.ArgumentParser(description="Compare dimensions between two LAS files")
96
+ parser.add_argument("file1", type=str, help="Path to first LAS file")
97
+ parser.add_argument("file2", type=str, help="Path to second LAS file")
98
+ parser.add_argument(
99
+ "--dimensions", nargs="*", help="List of dimensions to compare. If not specified, compares all dimensions."
100
+ )
101
+
91
102
  args = parser.parse_args()
92
-
103
+
93
104
  file1 = Path(args.file1)
94
105
  file2 = Path(args.file2)
95
-
106
+
96
107
  if not file1.exists() or not file2.exists():
97
108
  print("Error: One or both files do not exist")
98
109
  exit(1)
99
-
110
+
100
111
  result = compare_las_dimensions(file1, file2, args.dimensions)
101
- print(f"Dimensions comparison result: {'identical' if result else 'different'}")
112
+ print(f"Dimensions comparison result: {'identical' if result[0] else 'different'}")
102
113
  return result
103
114
 
115
+
104
116
  if __name__ == "__main__":
105
- main()
117
+ main()
@@ -4,13 +4,12 @@ import argparse
4
4
  import json
5
5
  import logging
6
6
  import os
7
- import tempfile
8
7
  from collections import Counter
9
8
  from typing import Dict, List
10
9
 
11
10
  import pdal
12
11
 
13
- from pdaltools.standardize_format import exec_las2las, get_writer_parameters
12
+ from pdaltools.standardize_format import get_writer_parameters
14
13
  from pdaltools.unlock_file import copy_and_hack_decorator
15
14
 
16
15
 
@@ -106,26 +105,13 @@ def parse_replacement_map_from_path_or_json_string(replacement_map):
106
105
  return parsed_map
107
106
 
108
107
 
109
- def replace_values_clean(
110
- input_file: str,
111
- output_file: str,
112
- replacement_map: Dict,
113
- attribute: str = "Classification",
114
- writer_parameters: Dict = {},
115
- ):
116
- filename = os.path.basename(output_file)
117
- with tempfile.NamedTemporaryFile(suffix=filename) as tmp:
118
- replace_values(input_file, tmp.name, replacement_map, attribute, writer_parameters)
119
- exec_las2las(tmp.name, output_file)
120
-
121
-
122
108
  def main():
123
109
  args = parse_args()
124
110
  writer_params_from_parser = dict(dataformat_id=args.record_format, a_srs=args.projection)
125
111
  writer_parameters = get_writer_parameters(writer_params_from_parser)
126
112
  replacement_map = parse_replacement_map_from_path_or_json_string(args.replacement_map)
127
113
 
128
- replace_values_clean(args.input_file, args.output_file, replacement_map, args.attribute, writer_parameters)
114
+ replace_values(args.input_file, args.output_file, replacement_map, args.attribute, writer_parameters)
129
115
 
130
116
 
131
117
  if __name__ == "__main__":
@@ -9,9 +9,6 @@
9
9
  """
10
10
 
11
11
  import argparse
12
- import os
13
- import platform
14
- import subprocess as sp
15
12
  import tempfile
16
13
  from typing import Dict, List
17
14
 
@@ -79,8 +76,8 @@ def get_writer_parameters(new_parameters: Dict) -> Dict:
79
76
  params = STANDARD_PARAMETERS | new_parameters
80
77
  return params
81
78
 
82
-
83
- def rewrite_with_pdal(
79
+ @copy_and_hack_decorator
80
+ def standardize(
84
81
  input_file: str, output_file: str, params_from_parser: Dict, classes_to_remove: List = [], rename_dims: List = []
85
82
  ) -> None:
86
83
  params = get_writer_parameters(params_from_parser)
@@ -109,32 +106,6 @@ def rewrite_with_pdal(
109
106
  pipeline.execute()
110
107
 
111
108
 
112
- def exec_las2las(input_file: str, output_file: str):
113
- if platform.processor() == "arm" and platform.architecture()[0] == "64bit":
114
- las2las = "las2las64"
115
- else:
116
- las2las = "las2las"
117
- r = sp.run([las2las, "-i", input_file, "-o", output_file], stderr=sp.PIPE, stdout=sp.PIPE)
118
- if r.returncode == 1:
119
- msg = r.stderr.decode()
120
- print(msg)
121
- raise RuntimeError(msg)
122
-
123
- output = r.stdout.decode()
124
- for line in output.splitlines():
125
- print(line)
126
-
127
-
128
- @copy_and_hack_decorator
129
- def standardize(
130
- input_file: str, output_file: str, params_from_parser: Dict, class_points_removed: [], rename_dims: []
131
- ) -> None:
132
- filename = os.path.basename(output_file)
133
- with tempfile.NamedTemporaryFile(suffix=filename) as tmp:
134
- rewrite_with_pdal(input_file, tmp.name, params_from_parser, class_points_removed, rename_dims)
135
- exec_las2las(tmp.name, output_file)
136
-
137
-
138
109
  def main():
139
110
  args = parse_args()
140
111
  params_from_parser = dict(