ign-pdal-tools 1.15.6__tar.gz → 1.15.8__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 (45) hide show
  1. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/PKG-INFO +1 -1
  2. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/ign_pdal_tools.egg-info/PKG-INFO +1 -1
  3. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/_version.py +1 -1
  4. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/color.py +108 -26
  5. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/las_comparison.py +70 -14
  6. ign_pdal_tools-1.15.8/pdaltools/las_merge.py +144 -0
  7. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_color.py +72 -16
  8. ign_pdal_tools-1.15.8/test/test_las_comparison.py +585 -0
  9. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_las_merge.py +21 -1
  10. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_unlock.py +2 -2
  11. ign_pdal_tools-1.15.6/pdaltools/las_merge.py +0 -109
  12. ign_pdal_tools-1.15.6/test/test_las_comparison.py +0 -286
  13. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/LICENSE.md +0 -0
  14. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/README.md +0 -0
  15. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/ign_pdal_tools.egg-info/SOURCES.txt +0 -0
  16. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/ign_pdal_tools.egg-info/dependency_links.txt +0 -0
  17. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/ign_pdal_tools.egg-info/top_level.txt +0 -0
  18. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/add_points_in_pointcloud.py +0 -0
  19. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/create_random_laz.py +0 -0
  20. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/download_image.py +0 -0
  21. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/las_add_buffer.py +0 -0
  22. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/las_clip.py +0 -0
  23. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/las_info.py +0 -0
  24. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/las_remove_dimensions.py +0 -0
  25. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/las_rename_dimension.py +0 -0
  26. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/pcd_info.py +0 -0
  27. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/replace_area_in_pointcloud.py +0 -0
  28. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/replace_attribute_in_las.py +0 -0
  29. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/standardize_format.py +0 -0
  30. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pdaltools/unlock_file.py +0 -0
  31. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/pyproject.toml +0 -0
  32. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/setup.cfg +0 -0
  33. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_add_points_in_pointcloud.py +0 -0
  34. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_create_random_laz.py +0 -0
  35. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_download_image.py +0 -0
  36. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_las_add_buffer.py +0 -0
  37. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_las_clip.py +0 -0
  38. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_las_info.py +0 -0
  39. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_las_remove_dimensions.py +0 -0
  40. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_las_rename_dimension.py +0 -0
  41. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_pcd_info.py +0 -0
  42. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_pdal.py +0 -0
  43. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_replace_area_in_pointcloud.py +0 -0
  44. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_replace_attribute_in_las.py +0 -0
  45. {ign_pdal_tools-1.15.6 → ign_pdal_tools-1.15.8}/test/test_standardize_format.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ign-pdal-tools
3
- Version: 1.15.6
3
+ Version: 1.15.8
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ign-pdal-tools
3
- Version: 1.15.6
3
+ Version: 1.15.8
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,4 +1,4 @@
1
- __version__ = "1.15.6"
1
+ __version__ = "1.15.8"
2
2
 
3
3
 
4
4
  if __name__ == "__main__":
@@ -31,7 +31,7 @@ def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: f
31
31
  return min_d, max_d
32
32
 
33
33
 
34
- def color(
34
+ def color_from_stream(
35
35
  input_file: str,
36
36
  output_file: str,
37
37
  proj="",
@@ -156,28 +156,58 @@ def color(
156
156
  return tmp_ortho, tmp_ortho_irc
157
157
 
158
158
 
159
- def parse_args():
160
- parser = argparse.ArgumentParser("Colorize tool", formatter_class=argparse.RawTextHelpFormatter)
161
- parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
162
- parser.add_argument("--output", "-o", type=str, default="", help="Output file")
163
- parser.add_argument(
164
- "--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
159
+ def color_from_files(
160
+ input_file: str,
161
+ output_file: str,
162
+ rgb_image: str,
163
+ irc_image: str,
164
+ color_rvb_enabled=True,
165
+ color_ir_enabled=True,
166
+ veget_index_file="",
167
+ vegetation_dim="Deviation",
168
+ ):
169
+ pipeline = pdal.Reader.las(filename=input_file)
170
+
171
+ writer_extra_dims = "all"
172
+
173
+ if veget_index_file and veget_index_file != "":
174
+ print(f"Remplissage du champ {vegetation_dim} à partir du fichier {veget_index_file}")
175
+ pipeline |= pdal.Filter.colorization(raster=veget_index_file, dimensions=f"{vegetation_dim}:1:256.0")
176
+ writer_extra_dims = [f"{vegetation_dim}=ushort"]
177
+
178
+ # Warning: the initial color is multiplied by 256 despite its initial 8-bits encoding
179
+ # which turns it to a 0 to 255*256 range.
180
+ # It is kept this way because of other dependencies that have been tuned to fit this range
181
+ if color_rvb_enabled:
182
+ pipeline |= pdal.Filter.colorization(raster=rgb_image, dimensions="Red:1:256.0, Green:2:256.0, Blue:3:256.0")
183
+ if color_ir_enabled:
184
+ pipeline |= pdal.Filter.colorization(raster=irc_image, dimensions="Infrared:1:256.0")
185
+
186
+ pipeline |= pdal.Writer.las(
187
+ filename=output_file, extra_dims=writer_extra_dims, minor_version="4", dataformat_id="8", forward="all"
165
188
  )
166
- parser.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
167
- parser.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
168
- parser.add_argument("--rvb", action="store_true", help="Colorize RVB")
169
- parser.add_argument("--ir", action="store_true", help="Colorize IR")
170
- parser.add_argument(
171
- "--vegetation",
172
- type=str,
173
- default="",
174
- help="Vegetation file (raster), value will be stored in 'vegetation_dim' field",
189
+
190
+ print("Traitement du nuage de point")
191
+ pipeline.execute()
192
+
193
+
194
+ def argument_parser():
195
+ parser = argparse.ArgumentParser("Colorize tool")
196
+ subparsers = parser.add_subparsers(required=True)
197
+
198
+ # first command is 'from_stream'
199
+ from_stream = subparsers.add_parser("from_stream", help="Images are downloaded from streams")
200
+ from_stream.add_argument(
201
+ "--proj", "-p", type=str, default="", help="Projection, default will use projection from metadata input"
175
202
  )
176
- parser.add_argument(
177
- "--vegetation_dim", type=str, default="Deviation", help="name of the extra_dim uses for the vegetation value"
203
+ from_stream.add_argument("--timeout", "-t", type=int, default=300, help="Timeout, in seconds")
204
+ from_stream.add_argument("--rvb", action="store_true", help="Colorize RVB")
205
+ from_stream.add_argument("--ir", action="store_true", help="Colorize IR")
206
+ from_stream.add_argument("--resolution", "-r", type=float, default=5, help="Resolution, in pixel per meter")
207
+ from_stream.add_argument(
208
+ "--check-images", "-c", action="store_true", help="Check that downloaded image is not white"
178
209
  )
179
- parser.add_argument("--check-images", "-c", action="store_true", help="Check that downloaded image is not white")
180
- parser.add_argument(
210
+ from_stream.add_argument(
181
211
  "--stream-RGB",
182
212
  type=str,
183
213
  default="ORTHOIMAGERY.ORTHOPHOTOS",
@@ -186,27 +216,49 @@ default stream (ORTHOIMAGERY.ORTHOPHOTOS) let the server choose the resolution
186
216
  for 20cm resolution rasters, use HR.ORTHOIMAGERY.ORTHOPHOTOS
187
217
  for 50 cm resolution rasters, use ORTHOIMAGERY.ORTHOPHOTOS.BDORTHO""",
188
218
  )
189
- parser.add_argument(
219
+ from_stream.add_argument(
190
220
  "--stream-IRC",
191
221
  type=str,
192
222
  default="ORTHOIMAGERY.ORTHOPHOTOS.IRC",
193
223
  help="""WMS raster stream for IRC colorization. Default to ORTHOIMAGERY.ORTHOPHOTOS.IRC
194
224
  Documentation about possible stream : https://geoservices.ign.fr/services-web-experts-ortho""",
195
225
  )
196
- parser.add_argument(
226
+ from_stream.add_argument(
197
227
  "--size-max-GPF",
198
228
  type=int,
199
229
  default=5000,
200
230
  help="Maximum edge size (in pixels) of downloaded images."
201
231
  " If input file needs more, several images are downloaded and merged.",
202
232
  )
233
+ add_common_options(from_stream)
234
+ from_stream.set_defaults(func=from_stream_func)
203
235
 
204
- return parser.parse_args()
236
+ # second command is 'from_files'
237
+ from_files = subparsers.add_parser("from_files", help="Images are in directories from RGB/IRC")
238
+ from_files.add_argument("--image_RGB", type=str, required=True, help="RGB image filepath")
239
+ from_files.add_argument("--image_IRC", type=str, required=True, help="IRC image filepath")
240
+ add_common_options(from_files)
241
+ from_files.set_defaults(func=from_files_func)
205
242
 
243
+ return parser
206
244
 
207
- if __name__ == "__main__":
208
- args = parse_args()
209
- color(
245
+
246
+ def add_common_options(parser):
247
+ parser.add_argument("--input", "-i", type=str, required=True, help="Input file")
248
+ parser.add_argument("--output", "-o", type=str, default="", help="Output file")
249
+ parser.add_argument(
250
+ "--vegetation",
251
+ type=str,
252
+ default="",
253
+ help="Vegetation file (raster), value will be stored in 'vegetation_dim' field",
254
+ )
255
+ parser.add_argument(
256
+ "--vegetation_dim", type=str, default="Deviation", help="name of the extra_dim uses for the vegetation value"
257
+ )
258
+
259
+
260
+ def from_stream_func(args):
261
+ color_from_stream(
210
262
  input_file=args.input,
211
263
  output_file=args.output,
212
264
  proj=args.proj,
@@ -221,3 +273,33 @@ if __name__ == "__main__":
221
273
  stream_IRC=args.stream_IRC,
222
274
  size_max_gpf=args.size_max_GPF,
223
275
  )
276
+
277
+
278
+ def from_files_func(args):
279
+ if args.image_RGB and args.image_RGB != "":
280
+ color_rvb_enabled = True
281
+ else:
282
+ color_rvb_enabled = False
283
+ if args.image_IRC and args.image_IRC != "":
284
+ color_irc_enabled = True
285
+ else:
286
+ color_irc_enabled = False
287
+
288
+ if not color_rvb_enabled and not color_irc_enabled:
289
+ raise ValueError("At least one of --rvb or --ir must be provided")
290
+
291
+ color_from_files(
292
+ input_file=args.input,
293
+ output_file=args.output,
294
+ rgb_image=args.image_RGB,
295
+ irc_image=args.image_IRC,
296
+ color_rvb_enabled=color_rvb_enabled,
297
+ color_ir_enabled=color_irc_enabled,
298
+ veget_index_file=args.vegetation,
299
+ vegetation_dim=args.vegetation_dim,
300
+ )
301
+
302
+
303
+ if __name__ == "__main__":
304
+ args = argument_parser.parse_args()
305
+ args.func(args)
@@ -1,12 +1,13 @@
1
1
  import argparse
2
- from pathlib import Path
3
- from typing import Tuple
2
+ from typing import Tuple, Dict, Optional
4
3
 
5
4
  import laspy
6
5
  import numpy as np
7
6
 
7
+ from pathlib import Path
8
+
8
9
 
9
- def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) -> Tuple[bool, int, float]:
10
+ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None, precision: Optional[Dict[str, float]] = None) -> Tuple[bool, int, float]:
10
11
  """
11
12
  Compare specified dimensions between two LAS files.
12
13
  If no dimensions are specified, compares all available dimensions.
@@ -16,6 +17,8 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
16
17
  file1: Path to the first LAS file
17
18
  file2: Path to the second LAS file
18
19
  dimensions: List of dimension names to compare (optional)
20
+ precision: Dictionary mapping dimension names to tolerance values for float comparison.
21
+ If None or dimension not in dict, uses exact comparison (default: None)
19
22
 
20
23
  Returns:
21
24
  bool: True if all specified dimensions are identical, False otherwise
@@ -59,20 +62,42 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
59
62
  # Compare each dimension
60
63
  for dim in dimensions:
61
64
  try:
65
+
62
66
  # Get sorted dimension arrays
63
67
  dim1 = np.array(las1[dim])[sort_idx1]
64
68
  dim2 = np.array(las2[dim])[sort_idx2]
65
69
 
70
+ # Get precision for this dimension (if specified)
71
+ dim_precision = None
72
+ if precision is not None and dim in precision:
73
+ dim_precision = precision[dim]
74
+
66
75
  # Compare dimensions
67
- if not np.array_equal(dim1, dim2):
68
- # Find differences
69
- diff_indices = np.where(dim1 != dim2)[0]
70
- print(f"Found {len(diff_indices)} points with different {dim}:")
71
- for idx in diff_indices[:10]: # Show first 10 differences
72
- print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}")
73
- if len(diff_indices) > 10:
74
- print(f"... and {len(diff_indices) - 10} more differences")
75
- return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
76
+ if dim_precision is not None:
77
+ # Use tolerance-based comparison for floats
78
+ are_equal = np.allclose(dim1, dim2, rtol=0, atol=dim_precision)
79
+ if not are_equal:
80
+ # Find differences
81
+ diff_mask = ~np.isclose(dim1, dim2, rtol=0, atol=dim_precision)
82
+ diff_indices = np.where(diff_mask)[0]
83
+ print(f"Found {len(diff_indices)} points with different {dim} (tolerance={dim_precision}):")
84
+ for idx in diff_indices[:10]: # Show first 10 differences
85
+ diff_value = abs(dim1[idx] - dim2[idx])
86
+ print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}, diff={diff_value}")
87
+ if len(diff_indices) > 10:
88
+ print(f"... and {len(diff_indices) - 10} more differences")
89
+ return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
90
+ else:
91
+ # Exact comparison
92
+ if not np.array_equal(dim1, dim2):
93
+ # Find differences
94
+ diff_indices = np.where(dim1 != dim2)[0]
95
+ print(f"Found {len(diff_indices)} points with different {dim}:")
96
+ for idx in diff_indices[:10]: # Show first 10 differences
97
+ print(f"Point {idx}: file1={dim1[idx]}, file2={dim2[idx]}")
98
+ if len(diff_indices) > 10:
99
+ print(f"... and {len(diff_indices) - 10} more differences")
100
+ return False, len(diff_indices), 100 * len(diff_indices) / len(las1)
76
101
 
77
102
  except KeyError:
78
103
  print(f"Dimension '{dim}' not found in one or both files")
@@ -93,12 +118,32 @@ def compare_las_dimensions(file1: Path, file2: Path, dimensions: list = None) ->
93
118
 
94
119
  # Update main function to use the new compare function
95
120
  def main():
96
- parser = argparse.ArgumentParser(description="Compare dimensions between two LAS files")
121
+ parser = argparse.ArgumentParser(
122
+ description="Compare dimensions between two LAS files",
123
+ formatter_class=argparse.RawDescriptionHelpFormatter,
124
+ epilog="""
125
+ Examples:
126
+ # Compare all dimensions with exact match
127
+ python las_comparison.py file1.las file2.las
128
+
129
+ # Compare specific dimensions with precision per dimension
130
+ python las_comparison.py file1.las file2.las --dimensions X Y Z --precision X=0.001 Y=0.001 Z=0.0001
131
+
132
+ # Compare all dimensions with precision for specific ones
133
+ python las_comparison.py file1.las file2.las --precision X=0.001 Y=0.001
134
+ """
135
+ )
97
136
  parser.add_argument("file1", type=str, help="Path to first LAS file")
98
137
  parser.add_argument("file2", type=str, help="Path to second LAS file")
99
138
  parser.add_argument(
100
139
  "--dimensions", nargs="*", help="List of dimensions to compare. If not specified, compares all dimensions."
101
140
  )
141
+ parser.add_argument(
142
+ "--precision", nargs="*", metavar="DIM=VAL",
143
+ help="Tolerance for float comparison per dimension (format: DIMENSION=PRECISION). "
144
+ "Example: --precision X=0.001 Y=0.001 Z=0.0001. "
145
+ "Dimensions not specified will use exact comparison."
146
+ )
102
147
 
103
148
  args = parser.parse_args()
104
149
 
@@ -109,7 +154,18 @@ def main():
109
154
  print("Error: One or both files do not exist")
110
155
  exit(1)
111
156
 
112
- result = compare_las_dimensions(file1, file2, args.dimensions)
157
+ # Parse precision dictionary from command line arguments
158
+ precision_dict = None
159
+ if args.precision:
160
+ precision_dict = {}
161
+ for prec_spec in args.precision:
162
+ try:
163
+ dim_name, prec_value = prec_spec.split('=', 1)
164
+ precision_dict[dim_name] = float(prec_value)
165
+ except ValueError:
166
+ parser.error(f"Invalid precision format: '{prec_spec}'. Expected format: DIMENSION=PRECISION (e.g., X=0.001)")
167
+
168
+ result = compare_las_dimensions(file1, file2, args.dimensions, precision_dict)
113
169
  print(f"Dimensions comparison result: {'identical' if result[0] else 'different'}")
114
170
  return result
115
171
 
@@ -0,0 +1,144 @@
1
+ import logging
2
+ import os
3
+
4
+ import pdal
5
+
6
+ from pdaltools.las_info import parse_filename
7
+
8
+
9
+ def create_filenames_suffixes(file: str, tile_width: int = 1000, tile_coord_scale: int = 1000):
10
+ """Generate the name of the tiles around the input LIDAR tile
11
+ It supposes that the file names are formatted as {prefix1}_{prefix2}_{coordx}_{coordy}_{suffix}
12
+ with coordx and coordy having at least 4 digits
13
+
14
+ For example Semis_2021_0000_1111_LA93_IGN69.las
15
+
16
+ Generates only the suffix part of the filename, for example, for file like above, it will generate:
17
+ _0000_1112_LA93_IGN69.las
18
+ _0001_1112_LA93_IGN69.las
19
+ ...
20
+
21
+ Args:
22
+ file(str): name of LIDAR file
23
+ tile width (int): width of tiles in meters (usually 1000m)
24
+ tile_coord_scale (int) : scale used in the filename to describe coordinates in meters
25
+ (usually 1000m)
26
+ Returns:
27
+ list_input(list): List of LIDAR's filename suffix.
28
+ """
29
+
30
+ # Create name of LIDAR tiles who cercle the tile
31
+ # # Parameters
32
+ _prefix, coord_x, coord_y, _suffix = parse_filename(file)
33
+ offset = int(tile_width / tile_coord_scale)
34
+ # On left
35
+ _tile_hl = f"_{(coord_x - offset):04d}_{(coord_y + offset):04d}_{_suffix}"
36
+ _tile_ml = f"_{(coord_x - offset):04d}_{coord_y:04d}_{_suffix}"
37
+ _tile_bl = f"_{(coord_x - offset):04d}_{(coord_y - offset):04d}_{_suffix}"
38
+ # On Right
39
+ _tile_hr = f"_{(coord_x + offset):04d}_{(coord_y + offset):04d}_{_suffix}"
40
+ _tile_mr = f"_{(coord_x + offset):04d}_{coord_y:04d}_{_suffix}"
41
+ _tile_br = f"_{(coord_x + offset):04d}_{(coord_y - offset):04d}_{_suffix}"
42
+ # Above
43
+ _tile_a = f"_{coord_x:04d}_{(coord_y + offset):04d}_{_suffix}"
44
+ # Below
45
+ _tile_b = f"_{coord_x:04d}_{(coord_y - offset):04d}_{_suffix}"
46
+ # Return the severals tile's names
47
+ return _tile_hl, _tile_ml, _tile_bl, _tile_a, _tile_b, _tile_hr, _tile_mr, _tile_br
48
+
49
+
50
+ def match_suffix_with_filenames(suffix_list: list, all_files: list, las_dir: str):
51
+ """Match suffix list with real filenames
52
+ Args:
53
+ suffix_list (list): List of suffix patterns to match
54
+ all_files (list): List of all files in las_dir
55
+ las_dir (str): Directory of pointclouds
56
+
57
+ Returns:
58
+ las_list(List): List of matched files
59
+ """
60
+ las_list = []
61
+ for suffix in suffix_list:
62
+ matches = [filename for filename in all_files if filename.endswith(suffix)]
63
+ if len(matches) == 0:
64
+ logging.info(f"NOK : {suffix}")
65
+ else:
66
+ # in case of multiple matches, select the most recent year (ex: Semis_2021_ before Semis_2020_ )
67
+ matches.sort(reverse=True)
68
+ selected = matches[0]
69
+ if len(matches) > 1:
70
+ logging.warning(f"Multiple matches for {suffix} : {matches} ; taking {selected}")
71
+
72
+ # Append full path
73
+ las_list.append(os.path.join(las_dir, selected))
74
+ return las_list
75
+
76
+
77
+ def create_tiles_list(all_files, las_dir, input_file, tile_width=1000, tile_coord_scale=1000):
78
+ """Return the paths of 8 tiles around the tile + the input tile
79
+ Args:
80
+ all_files (list): list of all files in las_dir
81
+ las_dir (str): directory of pointclouds
82
+ input_file (str): path to queried LIDAR tile
83
+ tile_width (int): Width of a tile(in the reference unit: 1m)
84
+ tile_coord_scale (int): Scale used in filename to describe coordinates (usually kilometers)
85
+ 1000 * 1m (with 1m being the reference)
86
+
87
+ Returns:
88
+ list_files: list of tiles
89
+ """
90
+
91
+ # Return list 8 tiles around the tile, but only the suffix part of the name.
92
+ suffix_list = create_filenames_suffixes(os.path.basename(input_file), tile_width, tile_coord_scale)
93
+
94
+ # Match suffix patterns with real files
95
+ list_files = match_suffix_with_filenames(suffix_list, all_files, las_dir)
96
+
97
+ # Appending queried tile to list
98
+ list_files.append(input_file)
99
+
100
+ return list_files
101
+
102
+
103
+ def create_list(las_dir, input_file, tile_width=1000, tile_coord_scale=1000):
104
+ """Return the paths of 8 tiles around the tile + the input tile
105
+ Args:
106
+ las_dir (str): directory of pointclouds
107
+ input_file (str): path to queried LIDAR tile
108
+ tile_width (int): Width of a tile(in the reference unit: 1m)
109
+ tile_coord_scale (int): Scale used in filename to describe coordinates (usually kilometers)
110
+ 1000 * 1m (with 1m being the reference)
111
+
112
+ Returns:
113
+ list_files: list of tiles
114
+ """
115
+
116
+ # list files on the disk
117
+ all_files = os.listdir(las_dir)
118
+
119
+ # call the function with the list of files
120
+ return create_tiles_list(all_files, las_dir, input_file, tile_width, tile_coord_scale)
121
+
122
+
123
+ def las_merge(las_dir, input_file, merge_file, tile_width=1000, tile_coord_scale=1000):
124
+ """Merge LIDAR tiles around input_file tile
125
+ Args:
126
+ las_dir (str): directory of pointclouds (to look for neigboprs)
127
+ input_file (str): name of query LIDAR file (with extension)
128
+ output_file (str): path to output
129
+ tile_width (int): Width of a tile(in the reference unit: 1m)
130
+ tile_coord_scale (int): Scale used in filename to describe coordinates (usually kilometers)
131
+ 1000 * 1m (with 1m being the reference)
132
+ """
133
+ # List files to merge
134
+ files = create_list(las_dir, input_file, tile_width, tile_coord_scale)
135
+ if len(files) > 0:
136
+ # Merge
137
+ pipeline = pdal.Pipeline()
138
+ for f in files:
139
+ pipeline |= pdal.Reader.las(filename=f)
140
+ pipeline |= pdal.Filter.merge()
141
+ pipeline |= pdal.Writer.las(filename=merge_file, forward="all")
142
+ pipeline.execute()
143
+ else:
144
+ raise ValueError("List of valid tiles is empty : stop processing")
@@ -7,6 +7,7 @@ import numpy as np
7
7
  import pytest
8
8
 
9
9
  from pdaltools import color
10
+ from pdaltools.color import argument_parser
10
11
 
11
12
  cwd = os.getcwd()
12
13
 
@@ -14,6 +15,11 @@ TEST_PATH = os.path.dirname(os.path.abspath(__file__))
14
15
  TMPDIR = os.path.join(TEST_PATH, "tmp", "color")
15
16
 
16
17
  INPUT_PATH = os.path.join(TEST_PATH, "data/test_noepsg_043500_629205_IGN69.laz")
18
+ INPUT_PATH_TILE = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
19
+
20
+ RGB_IMAGE = os.path.join(TEST_PATH, "data/color/test_data_rgb.tif")
21
+ IRC_IMAGE = os.path.join(TEST_PATH, "data/color/test_data_irc.tif")
22
+
17
23
 
18
24
  OUTPUT_FILE = os.path.join(TMPDIR, "Semis_2021_0435_6292_LA93_IGN69.colorized.las")
19
25
  EPSG = "2154"
@@ -33,12 +39,12 @@ def test_epsg_fail():
33
39
  RuntimeError,
34
40
  match="EPSG could not be inferred from metadata: No 'srs' key in metadata.",
35
41
  ):
36
- color.color(INPUT_PATH, OUTPUT_FILE, "", 0.1, 15)
42
+ color.color_from_stream(INPUT_PATH, OUTPUT_FILE, "", 0.1, 15)
37
43
 
38
44
 
39
45
  @pytest.mark.geopf
40
46
  def test_color_and_keeping_orthoimages():
41
- tmp_ortho, tmp_ortho_irc = color.color(INPUT_PATH, OUTPUT_FILE, EPSG, check_images=True)
47
+ tmp_ortho, tmp_ortho_irc = color.color_from_stream(INPUT_PATH, OUTPUT_FILE, EPSG, check_images=True)
42
48
  assert Path(tmp_ortho.name).exists()
43
49
  assert Path(tmp_ortho_irc.name).exists()
44
50
 
@@ -63,7 +69,7 @@ def test_color_narrow_cloud():
63
69
  input_path = os.path.join(TEST_PATH, "data/test_data_0436_6384_LA93_IGN69_single_point.laz")
64
70
  output_path = os.path.join(TMPDIR, "color_narrow_cloud_test_data_0436_6384_LA93_IGN69_single_point.colorized.laz")
65
71
  # Test that clouds that are smaller in width or height to 20cm are still colorized without an error.
66
- color.color(input_path, output_path, EPSG)
72
+ color.color_from_stream(input_path, output_path, EPSG)
67
73
  with laspy.open(output_path, "r") as las:
68
74
  las_data = las.read()
69
75
  # Check all points are colored
@@ -75,10 +81,9 @@ def test_color_narrow_cloud():
75
81
 
76
82
  @pytest.mark.geopf
77
83
  def test_color_standard_cloud():
78
- input_path = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
79
84
  output_path = os.path.join(TMPDIR, "color_standard_cloud_test_data_77055_627760_LA93_IGN69.colorized.laz")
80
85
  # Test that clouds that are smaller in width or height to 20cm are still colorized without an error.
81
- color.color(input_path, output_path, EPSG)
86
+ color.color_from_stream(INPUT_PATH_TILE, output_path, EPSG)
82
87
  with laspy.open(output_path, "r") as las:
83
88
  las_data = las.read()
84
89
  # Check all points are colored
@@ -92,7 +97,7 @@ def test_color_epsg_2975_forced():
92
97
  input_path = os.path.join(TEST_PATH, "data/sample_lareunion_epsg2975.laz")
93
98
  output_path = os.path.join(TMPDIR, "color_epsg_2975_forced_sample_lareunion_epsg2975.colorized.laz")
94
99
 
95
- color.color(input_path, output_path, 2975)
100
+ color.color_from_stream(input_path, output_path, 2975)
96
101
 
97
102
 
98
103
  # the test is not working, the image is not detected as white
@@ -104,7 +109,7 @@ def test_color_epsg_2975_forced():
104
109
  # output_path = os.path.join(TMPDIR, "sample_lareunion_epsg2975.colorized.white.laz")#
105
110
 
106
111
  # with pytest.raises(ValueError) as excinfo:
107
- # color.color(input_path, output_path, check_images=True)
112
+ # color.color_from_stream(input_path, output_path, check_images=True)
108
113
 
109
114
  # assert "Downloaded image is white" in str(excinfo.value)
110
115
 
@@ -114,18 +119,17 @@ def test_color_epsg_2975_detected():
114
119
  input_path = os.path.join(TEST_PATH, "data/sample_lareunion_epsg2975.laz")
115
120
  output_path = os.path.join(TMPDIR, "color_epsg_2975_detected_sample_lareunion_epsg2975.colorized.laz")
116
121
  # Test that clouds that are smaller in width or height to 20cm are still clorized without an error.
117
- color.color(input_path, output_path)
122
+ color.color_from_stream(input_path, output_path)
118
123
 
119
124
 
120
125
  def test_color_vegetation_only():
121
- """Test the color() function with only vegetation"""
122
- input_path = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
126
+ """Test the color_from_stream() function with only vegetation"""
123
127
  output_path = os.path.join(TMPDIR, "test_color_vegetation.colorized.las")
124
128
  vegetation_path = os.path.join(TEST_PATH, "data/mock_vegetation.tif")
125
129
 
126
130
  # Test with all parameters explicitly defined
127
- color.color(
128
- input_file=input_path,
131
+ color.color_from_stream(
132
+ input_file=INPUT_PATH_TILE,
129
133
  output_file=output_path,
130
134
  proj="2154", # EPSG:2154 (Lambert 93)
131
135
  color_rvb_enabled=False, # RGB enabled
@@ -153,14 +157,13 @@ def test_color_vegetation_only():
153
157
 
154
158
  @pytest.mark.geopf
155
159
  def test_color_with_all_parameters():
156
- """Test the color() function with all parameters specified"""
157
- input_path = os.path.join(TEST_PATH, "data/test_data_77055_627760_LA93_IGN69.laz")
160
+ """Test the color_from_stream() function with all parameters specified"""
158
161
  output_path = os.path.join(TMPDIR, "test_color_all_params.colorized.las")
159
162
  vegetation_path = os.path.join(TEST_PATH, "data/mock_vegetation.tif")
160
163
 
161
164
  # Test with all parameters explicitly defined
162
- tmp_ortho, tmp_ortho_irc = color.color(
163
- input_file=input_path,
165
+ tmp_ortho, tmp_ortho_irc = color.color_from_stream(
166
+ input_file=INPUT_PATH_TILE,
164
167
  output_file=output_path,
165
168
  proj="2154", # EPSG:2154 (Lambert 93)
166
169
  pixel_per_meter=2.0, # custom resolution
@@ -196,3 +199,56 @@ def test_color_with_all_parameters():
196
199
  # Verify that the vegetation dimension is present
197
200
  assert "vegetation_dim" in las_data.point_format.dimension_names, "Vegetation dimension should be present"
198
201
  assert not np.all(las_data.vegetation_dim == 0), "Vegetation dimension should not be empty"
202
+
203
+
204
+ def test_color_from_files():
205
+ output_path = os.path.join(TMPDIR, "color_standard_cloud_files_test_data_77055_627760_LA93_IGN69.colorized.laz")
206
+
207
+ color.color_from_files(INPUT_PATH_TILE, output_path, RGB_IMAGE, IRC_IMAGE)
208
+
209
+ assert os.path.exists(output_path)
210
+
211
+ with laspy.open(output_path, "r") as las:
212
+ las_data = las.read()
213
+
214
+ # Verify that all points have been colorized (no 0 values)
215
+ las_rgb_missing = (las_data.red == 0) & (las_data.green == 0) & (las_data.blue == 0)
216
+ assert not np.any(las_rgb_missing), f"No point should have missing RGB, found {np.count_nonzero(las_rgb_missing)}"
217
+ assert not np.any(las_data.nir == 0), "No point should have missing NIR"
218
+
219
+
220
+ @pytest.mark.geopf
221
+ def test_main_from_stream():
222
+ output_file = os.path.join(TMPDIR, "main_from_stream", "output_main_from_stream.laz")
223
+ os.makedirs(os.path.dirname(output_file))
224
+ cmd = f"from_stream -i {INPUT_PATH_TILE} -o {output_file} -p {EPSG} --rvb --ir".split()
225
+ args = argument_parser().parse_args(cmd)
226
+ args.func(args)
227
+
228
+ assert os.path.exists(output_file)
229
+
230
+ with laspy.open(output_file, "r") as las:
231
+ las_data = las.read()
232
+
233
+ # Verify that all points have been colorized (no 0 values)
234
+ las_rgb_missing = (las_data.red == 0) & (las_data.green == 0) & (las_data.blue == 0)
235
+ assert not np.any(las_rgb_missing), f"No point should have missing RGB, found {np.count_nonzero(las_rgb_missing)}"
236
+ assert not np.any(las_data.nir == 0), "No point should have missing NIR"
237
+
238
+
239
+ def test_main_from_files():
240
+ output_file = os.path.join(TMPDIR, "main_from_files", "output_main_from_files.laz")
241
+ os.makedirs(os.path.dirname(output_file))
242
+ cmd = f"from_files -i {INPUT_PATH_TILE} -o {output_file} --image_RGB {RGB_IMAGE} --image_IRC {IRC_IMAGE}".split()
243
+ args = argument_parser().parse_args(cmd)
244
+ args.func(args)
245
+
246
+ assert os.path.exists(output_file)
247
+
248
+ with laspy.open(output_file, "r") as las:
249
+ las_data = las.read()
250
+
251
+ # Verify that all points have been colorized (no 0 values)
252
+ las_rgb_missing = (las_data.red == 0) & (las_data.green == 0) & (las_data.blue == 0)
253
+ assert not np.any(las_rgb_missing), f"No point should have missing RGB, found {np.count_nonzero(las_rgb_missing)}"
254
+ assert not np.any(las_data.nir == 0), "No point should have missing NIR"