ign-pdal-tools 1.15.0__tar.gz → 1.15.2__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 (43) hide show
  1. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/PKG-INFO +1 -1
  2. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/ign_pdal_tools.egg-info/PKG-INFO +1 -1
  3. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/_version.py +1 -1
  4. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/add_points_in_pointcloud.py +5 -0
  5. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/replace_area_in_pointcloud.py +13 -11
  6. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_add_points_in_pointcloud.py +51 -5
  7. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_replace_area_in_pointcloud.py +5 -4
  8. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/LICENSE.md +0 -0
  9. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/README.md +0 -0
  10. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/ign_pdal_tools.egg-info/SOURCES.txt +0 -0
  11. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/ign_pdal_tools.egg-info/dependency_links.txt +0 -0
  12. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/ign_pdal_tools.egg-info/top_level.txt +0 -0
  13. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/color.py +0 -0
  14. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/create_random_laz.py +0 -0
  15. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/download_image.py +0 -0
  16. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/las_add_buffer.py +0 -0
  17. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/las_clip.py +0 -0
  18. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/las_comparison.py +0 -0
  19. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/las_info.py +0 -0
  20. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/las_merge.py +0 -0
  21. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/las_remove_dimensions.py +0 -0
  22. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/las_rename_dimension.py +0 -0
  23. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/pcd_info.py +0 -0
  24. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/replace_attribute_in_las.py +0 -0
  25. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/standardize_format.py +0 -0
  26. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pdaltools/unlock_file.py +0 -0
  27. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/pyproject.toml +0 -0
  28. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/setup.cfg +0 -0
  29. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_color.py +0 -0
  30. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_create_random_laz.py +0 -0
  31. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_download_image.py +0 -0
  32. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_las_add_buffer.py +0 -0
  33. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_las_clip.py +0 -0
  34. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_las_comparison.py +0 -0
  35. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_las_info.py +0 -0
  36. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_las_merge.py +0 -0
  37. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_las_remove_dimensions.py +0 -0
  38. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_las_rename_dimension.py +0 -0
  39. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_pcd_info.py +0 -0
  40. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_pdal.py +0 -0
  41. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_replace_attribute_in_las.py +0 -0
  42. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_standardize_format.py +0 -0
  43. {ign_pdal_tools-1.15.0 → ign_pdal_tools-1.15.2}/test/test_unlock.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ign-pdal-tools
3
- Version: 1.15.0
3
+ Version: 1.15.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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ign-pdal-tools
3
- Version: 1.15.0
3
+ Version: 1.15.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,4 +1,4 @@
1
- __version__ = "1.15.0"
1
+ __version__ = "1.15.2"
2
2
 
3
3
 
4
4
  if __name__ == "__main__":
@@ -6,6 +6,7 @@ import geopandas as gpd
6
6
  import laspy
7
7
  import numpy as np
8
8
  import pdal
9
+ from shapely import set_precision
9
10
  from shapely.geometry import MultiPoint, Point, box
10
11
 
11
12
  from pdaltools.las_info import get_epsg_from_las, get_tile_bbox
@@ -327,6 +328,10 @@ def add_points_from_geometry_to_las(
327
328
  # Clip points from GeoJSON by LIDAR tile
328
329
  points_clipped = clip_3d_points_to_tile(points_gdf, input_las, spatial_ref, tile_width)
329
330
 
331
+ # Remove duplicate points (due to precision issue) - las file have centrimetric precision
332
+ points_clipped.geometry = set_precision(points_clipped.geometry, grid_size=0.01)
333
+ points_clipped = points_clipped.drop_duplicates()
334
+
330
335
  # Add points by LIDAR tile and save the result
331
336
  add_points_to_las(points_clipped, input_las, output_las, spatial_ref, virtual_points_classes)
332
337
 
@@ -33,13 +33,14 @@ def argument_parser():
33
33
  help="path of the source digital surface model (DSM), used to generate source points",
34
34
  )
35
35
  from_DSM.add_argument(
36
- "--source_ground_area",
36
+ "--source_ground_mask",
37
37
  "-g",
38
38
  required=True,
39
39
  type=str,
40
40
  help=(
41
- "area of the ground, used to intersect source cloud. "
42
- "(shapefile, geojson or other format readable by GDAL)"
41
+ "ground mask, a raster file used to filter source cloud. "
42
+ "Pixel with value > 0 is considered as ground, and define the source cloud we keep. "
43
+ "(tif or other raster format readable by GDAL)"
43
44
  ),
44
45
  )
45
46
  from_DSM.add_argument(
@@ -65,7 +66,7 @@ def add_common_options(parser):
65
66
  "-r",
66
67
  required=True,
67
68
  type=str,
68
- help="area to replace (shapefile, geojson or other format readable by GDAL)",
69
+ help="area to replace (shapefile, geojson or other vector format readable by GDAL)",
69
70
  )
70
71
  parser.add_argument("--output_cloud", "-o", required=True, type=str, help="output cloud file")
71
72
 
@@ -84,7 +85,7 @@ def from_DSM_func(args):
84
85
  replace_area(
85
86
  target_cloud=args.target_cloud,
86
87
  pipeline_source=pipeline_read_from_DSM(
87
- dsm=args.source_dsm, ground_area=args.source_ground_area, classification=args.source_classification
88
+ dsm=args.source_dsm, ground_mask=args.source_ground_mask, classification=args.source_classification
88
89
  ),
89
90
  replacement_area=args.replacement_area,
90
91
  output_cloud=args.output_cloud,
@@ -106,7 +107,7 @@ def pipeline_read_from_cloud(filename):
106
107
  return pipeline_source
107
108
 
108
109
 
109
- def pipeline_read_from_DSM(dsm, ground_area, classification):
110
+ def pipeline_read_from_DSM(dsm, ground_mask, classification):
110
111
  # get nodata value
111
112
  ds = gdal.Open(dsm)
112
113
  band = ds.GetRasterBand(1)
@@ -117,11 +118,11 @@ def pipeline_read_from_DSM(dsm, ground_area, classification):
117
118
  pipeline |= pdal.Reader.gdal(filename=dsm, header="Z")
118
119
  pipeline |= pdal.Filter.expression(expression=f"Z != {nodata_value}")
119
120
 
120
- pipeline |= pdal.Filter.ferry(dimensions="=> geometryFid")
121
- pipeline |= pdal.Filter.assign(assignment="geometryFid[:]=-1")
122
- pipeline |= pdal.Filter.overlay(column="fid", dimension="geometryFid", datasource=ground_area)
121
+ pipeline |= pdal.Filter.ferry(dimensions="=> ground")
122
+ pipeline |= pdal.Filter.assign(assignment="ground[:]=-1")
123
+ pipeline |= pdal.Filter.colorization(dimensions="ground:1:1.0", raster=ground_mask)
123
124
  # Keep only points in the area
124
- pipeline |= pdal.Filter.expression(expression="geometryFid>=0")
125
+ pipeline |= pdal.Filter.expression(expression="ground>0")
125
126
 
126
127
  # assign class
127
128
  pipeline |= pdal.Filter.ferry(dimensions="=>Classification")
@@ -209,4 +210,5 @@ def replace_area(
209
210
 
210
211
 
211
212
  if __name__ == "__main__":
212
- argument_parser().parse_args().func()
213
+ args = argument_parser().parse_args()
214
+ args.func(args)
@@ -109,7 +109,7 @@ def test_add_points_to_las(input_file, epsg, input_points_2d, expected_nb_points
109
109
  metadata_out = las_info_metadata(OUTPUT_FILE)
110
110
  output_dimensions = metadata_out["dimensions"]
111
111
 
112
- assert input_dimensions == output_dimensions # All dimension should be preserve
112
+ assert input_dimensions == output_dimensions # All dimension should be preserve
113
113
 
114
114
  point_count = compute_count_one_file(OUTPUT_FILE)["68"]
115
115
  assert point_count == expected_nb_points # Add all points from geojson
@@ -261,7 +261,7 @@ def test_generate_3d_points_from_lines(lines_gdf, spacing, altitude_column, expe
261
261
  INPUT_PCD,
262
262
  INPUT_LIGNES_2D_GEOJSON,
263
263
  "EPSG:2154",
264
- 678,
264
+ 677,
265
265
  0.25,
266
266
  "RecupZ",
267
267
  ), # should add only lines (.GeoJSON) within tile extend
@@ -269,16 +269,16 @@ def test_generate_3d_points_from_lines(lines_gdf, spacing, altitude_column, expe
269
269
  INPUT_PCD,
270
270
  INPUT_LIGNES_SHAPE,
271
271
  "EPSG:2154",
272
- 678,
272
+ 677,
273
273
  0.25,
274
274
  "RecupZ",
275
275
  ), # should add only lines (.shp) within tile extend
276
- (INPUT_PCD, INPUT_LIGNES_SHAPE, None, 678, 0.25, "RecupZ"), # Should work with or with an input epsg
276
+ (INPUT_PCD, INPUT_LIGNES_SHAPE, None, 677, 0.25, "RecupZ"), # Should work with or with an input epsg
277
277
  (
278
278
  INPUT_PCD,
279
279
  INPUT_LIGNES_3D_GEOJSON,
280
280
  None,
281
- 678,
281
+ 677,
282
282
  0.25,
283
283
  None,
284
284
  ), # Should work with or without an input epsg and without altitude_column
@@ -353,6 +353,52 @@ def test_add_points_from_geometry_to_las_nok(input_file, input_points, epsg, spa
353
353
  )
354
354
 
355
355
 
356
+ @pytest.mark.parametrize(
357
+ "spacing",
358
+ [
359
+ 0.1,
360
+ 0.01,
361
+ 0.001,
362
+ 0.25,
363
+ 0.5,
364
+ 1,
365
+ ],
366
+ )
367
+ def test_add_points_from_geometry_to_las_no_dupplicate(spacing):
368
+ # there should have no duplicate in final las
369
+
370
+ input_las_file = os.path.join(TEST_PATH, "data/crop_duplicate.laz")
371
+ input_geo_file = os.path.join(TEST_PATH, "data/crop_duplicate.geojson")
372
+
373
+ add_points_in_pointcloud.add_points_from_geometry_to_las(
374
+ input_geo_file, input_las_file, OUTPUT_FILE, 68, "EPSG:2154", 1000, spacing, None
375
+ )
376
+ assert Path(OUTPUT_FILE).exists() # check output exists
377
+
378
+ las = laspy.read(OUTPUT_FILE)
379
+
380
+ # Get all points with classification 68
381
+ class_68_points = las.points[las.classification == 68]
382
+ num_class_68_points = len(class_68_points)
383
+
384
+ # Print some information
385
+ print(f"Total points in file: {len(las.points)}")
386
+ print(f"Points with class 68: {num_class_68_points}")
387
+
388
+ # Verify we have some points with class 68
389
+ assert num_class_68_points > 0, "Expected to find points with class 68"
390
+
391
+ # Check for duplicate points (same X, Y, Z coordinates)
392
+ points_array = np.column_stack((class_68_points.x, class_68_points.y, class_68_points.z))
393
+ unique_points = np.unique(points_array, axis=0)
394
+
395
+ print(f"Number of unique points: {len(unique_points)}")
396
+ print(f"Number of total points: {len(points_array)}")
397
+
398
+ # Verify no duplicates
399
+ assert len(unique_points) == len(points_array), "Found duplicate points in class 68"
400
+
401
+
356
402
  def test_parse_args():
357
403
  # sanity check for arguments parsing
358
404
  args = add_points_in_pointcloud.parse_args(
@@ -30,7 +30,7 @@ SOURCE_CLOUD = os.path.join(INPUT_DIR, "source_cloud_crop.laz")
30
30
 
31
31
  # source may be a digital surface model
32
32
  SOURCE_DSM = os.path.join(INPUT_DIR, "DSM.tif")
33
- SOURCE_GROUND_AREA = os.path.join(INPUT_DIR, "ground_area.geojson")
33
+ SOURCE_GROUND_MASK = os.path.join(INPUT_DIR, "ground_mask.tif")
34
34
  SOURCE_CLASSIF = 68
35
35
 
36
36
  TMP_EXTRA_DIMS = os.path.join(TMP_PATH, "input_with_extra_dims")
@@ -285,7 +285,7 @@ def test_replace_area_with_no_output_point_with_extra_dims():
285
285
  def test_pipeline_read_from_DSM():
286
286
  cloud_from_DSM = os.path.join(TMP_PATH, "las_from_DSM.laz")
287
287
 
288
- pipeline = pipeline_read_from_DSM(dsm=SOURCE_DSM, ground_area=SOURCE_GROUND_AREA, classification=SOURCE_CLASSIF)
288
+ pipeline = pipeline_read_from_DSM(dsm=SOURCE_DSM, ground_mask=SOURCE_GROUND_MASK, classification=SOURCE_CLASSIF)
289
289
  pipeline |= pdal.Writer.las(cloud_from_DSM, forward="all", extra_dims="all")
290
290
  pipeline.execute()
291
291
 
@@ -326,9 +326,10 @@ def test_main_from_cloud_with_filter():
326
326
 
327
327
 
328
328
  def test_main_from_DSM():
329
- output_file = os.path.join(TMP_PATH, "output_main_from_cloud.laz")
329
+ output_file = os.path.join(TMP_PATH, "main_from_DSM", "output_main_from_DSM.laz")
330
+ os.makedirs(os.path.dirname(output_file))
330
331
  cmd = (
331
- f"from_DSM -d {SOURCE_DSM} -g {SOURCE_GROUND_AREA} -c {SOURCE_CLASSIF} -t {TARGET_CLOUD} -r {REPLACE_AREA}"
332
+ f"from_DSM -d {SOURCE_DSM} -g {SOURCE_GROUND_MASK} -c {SOURCE_CLASSIF} -t {TARGET_CLOUD} -r {REPLACE_AREA}"
332
333
  f" -o {output_file}"
333
334
  ).split()
334
335
  args = argument_parser().parse_args(cmd)