ign-pdal-tools 1.7.5__tar.gz → 1.7.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/PKG-INFO +2 -2
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/ign_pdal_tools.egg-info/PKG-INFO +2 -2
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/ign_pdal_tools.egg-info/SOURCES.txt +2 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/_version.py +1 -1
- ign_pdal_tools-1.7.6/pdaltools/add_points_in_pointcloud.py +102 -0
- ign_pdal_tools-1.7.6/test/test_add_points_in_pointcloud.py +82 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/LICENSE.md +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/README.md +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/ign_pdal_tools.egg-info/dependency_links.txt +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/ign_pdal_tools.egg-info/top_level.txt +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/add_points_in_las.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/color.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/las_add_buffer.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/las_clip.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/las_info.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/las_merge.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/las_remove_dimensions.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/pcd_info.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/replace_attribute_in_las.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/standardize_format.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pdaltools/unlock_file.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/pyproject.toml +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/setup.cfg +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_add_points_in_las.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_color.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_las_add_buffer.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_las_clip.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_las_info.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_las_merge.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_las_remove_dimensions.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_pcd_info.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_replace_attribute_in_las.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_standardize_format.py +0 -0
- {ign_pdal_tools-1.7.5 → ign_pdal_tools-1.7.6}/test/test_unlock.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: ign-pdal-tools
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.6
|
|
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
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: ign-pdal-tools
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.6
|
|
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
|
|
@@ -7,6 +7,7 @@ ign_pdal_tools.egg-info/dependency_links.txt
|
|
|
7
7
|
ign_pdal_tools.egg-info/top_level.txt
|
|
8
8
|
pdaltools/_version.py
|
|
9
9
|
pdaltools/add_points_in_las.py
|
|
10
|
+
pdaltools/add_points_in_pointcloud.py
|
|
10
11
|
pdaltools/color.py
|
|
11
12
|
pdaltools/las_add_buffer.py
|
|
12
13
|
pdaltools/las_clip.py
|
|
@@ -18,6 +19,7 @@ pdaltools/replace_attribute_in_las.py
|
|
|
18
19
|
pdaltools/standardize_format.py
|
|
19
20
|
pdaltools/unlock_file.py
|
|
20
21
|
test/test_add_points_in_las.py
|
|
22
|
+
test/test_add_points_in_pointcloud.py
|
|
21
23
|
test/test_color.py
|
|
22
24
|
test/test_las_add_buffer.py
|
|
23
25
|
test/test_las_clip.py
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import geopandas as gpd
|
|
2
|
+
import laspy
|
|
3
|
+
import numpy as np
|
|
4
|
+
from shapely.geometry import box
|
|
5
|
+
|
|
6
|
+
from pdaltools.las_info import get_tile_origin_using_header_info
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_tile_bbox(input_las, tile_width=1000) -> tuple:
|
|
10
|
+
"""
|
|
11
|
+
Get the theoretical bounding box (xmin, ymin, xmax, ymax) of a LIDAR tile
|
|
12
|
+
using its origin and the predefined tile width.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
input_las (str): Path to the LIDAR `.las/.laz` file.
|
|
16
|
+
tile_width (int): Width of the tile in meters (default: 1000).
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
tuple: Bounding box as (xmin, ymin, xmax, ymax).
|
|
20
|
+
"""
|
|
21
|
+
origin_x, origin_y = get_tile_origin_using_header_info(input_las)
|
|
22
|
+
bbox = (origin_x, origin_y - tile_width, origin_x + tile_width, origin_y)
|
|
23
|
+
return bbox
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def clip_3d_points_to_tile(input_points: str, input_las: str, crs: str) -> gpd.GeoDataFrame:
|
|
27
|
+
"""
|
|
28
|
+
Add points from a GeoJSON file in the LIDAR's tile.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
input_points (str): Path to the input GeoJSON file with 3D points.
|
|
32
|
+
input_las (str): Path to the LIDAR `.las/.laz` file.
|
|
33
|
+
crs (str): CRS of the data, e.g., 'EPSG:2154'.
|
|
34
|
+
|
|
35
|
+
Return:
|
|
36
|
+
gpd.GeoDataFrame: Points 2d with "Z" value
|
|
37
|
+
"""
|
|
38
|
+
# Compute the bounding box of the LIDAR tile
|
|
39
|
+
tile_bbox = get_tile_bbox(input_las)
|
|
40
|
+
|
|
41
|
+
# Read the input GeoJSON with 3D points
|
|
42
|
+
points_gdf = gpd.read_file(input_points)
|
|
43
|
+
|
|
44
|
+
# Ensure the CRS matches
|
|
45
|
+
if crs:
|
|
46
|
+
points_gdf = points_gdf.to_crs(crs)
|
|
47
|
+
|
|
48
|
+
# Create a polygon from the bounding box
|
|
49
|
+
bbox_polygon = box(*tile_bbox)
|
|
50
|
+
|
|
51
|
+
# Clip the points to the bounding box
|
|
52
|
+
clipped_points = points_gdf[points_gdf.intersects(bbox_polygon)].copy()
|
|
53
|
+
|
|
54
|
+
return clipped_points
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def add_points_to_las(
|
|
58
|
+
input_points_with_z: gpd.GeoDataFrame, input_las: str, output_las: str, virtual_points_classes=66
|
|
59
|
+
):
|
|
60
|
+
"""Add points (3D points in LAZ format) by LIDAR tiles (tiling file)
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
input_points_with_z(gpd.GeoDataFrame): geometry columns (2D points) as encoded to WKT.
|
|
64
|
+
input_las (str): Path to the LIDAR tiles (LAZ).
|
|
65
|
+
output_las (str): Path to save the updated LIDAR file (LAS/LAZ format).
|
|
66
|
+
virtual_points_classes (int): The classification value to assign to those virtual points (default: 66).
|
|
67
|
+
"""
|
|
68
|
+
# Check if input points are empty
|
|
69
|
+
if input_points_with_z.empty:
|
|
70
|
+
raise ValueError("No points to add. The input GeoDataFrame is empty.")
|
|
71
|
+
|
|
72
|
+
# Extract XYZ coordinates and additional attribute (classification)
|
|
73
|
+
x_coords = input_points_with_z.geometry.x
|
|
74
|
+
y_coords = input_points_with_z.geometry.y
|
|
75
|
+
z_coords = input_points_with_z.RecupZ
|
|
76
|
+
classes = virtual_points_classes * np.ones(len(input_points_with_z.index))
|
|
77
|
+
|
|
78
|
+
# Read the existing LIDAR file
|
|
79
|
+
with laspy.open(input_las, mode="r") as las:
|
|
80
|
+
las_data = las.read()
|
|
81
|
+
header = las.header
|
|
82
|
+
|
|
83
|
+
# Create a new header if the original header is missing or invalid
|
|
84
|
+
if header is None:
|
|
85
|
+
header = laspy.LasHeader(point_format=6, version="1.4") # Example format and version
|
|
86
|
+
|
|
87
|
+
# Append the clipped points to the existing LIDAR data
|
|
88
|
+
new_x = np.concatenate([las_data.x, x_coords])
|
|
89
|
+
new_y = np.concatenate([las_data.y, y_coords])
|
|
90
|
+
new_z = np.concatenate([las_data.z, z_coords])
|
|
91
|
+
new_classes = np.concatenate([las_data.classification, classes])
|
|
92
|
+
|
|
93
|
+
# Create a new LAS file with updated data
|
|
94
|
+
updated_las = laspy.LasData(header)
|
|
95
|
+
updated_las.x = new_x
|
|
96
|
+
updated_las.y = new_y
|
|
97
|
+
updated_las.z = new_z
|
|
98
|
+
updated_las.classification = new_classes
|
|
99
|
+
|
|
100
|
+
# Write the updated LAS file
|
|
101
|
+
with laspy.open(output_las, mode="w", header=header, do_compress=True) as writer:
|
|
102
|
+
writer.write_points(updated_las.points)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pdal
|
|
5
|
+
|
|
6
|
+
from pdaltools import add_points_in_pointcloud
|
|
7
|
+
|
|
8
|
+
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
|
|
9
|
+
TMP_PATH = os.path.join(TEST_PATH, "data/output")
|
|
10
|
+
DATA_LIDAR_PATH = os.path.join(TEST_PATH, "data/decimated_laz")
|
|
11
|
+
DATA_POINTS_PATH = os.path.join(TEST_PATH, "data/points_3d")
|
|
12
|
+
|
|
13
|
+
INPUT_FILE = os.path.join(DATA_LIDAR_PATH, "test_semis_2023_0292_6833_LA93_IGN69.laz")
|
|
14
|
+
INPUT_POINTS = os.path.join(DATA_POINTS_PATH, "Points_virtuels_0292_6833.geojson")
|
|
15
|
+
OUTPUT_FILE = os.path.join(TMP_PATH, "test_semis_2023_0292_6833_LA93_IGN69.laz")
|
|
16
|
+
|
|
17
|
+
INPUT_FILE_SMALL = os.path.join(DATA_LIDAR_PATH, "test_semis_2021_0382_6565_LA93_IGN69.laz")
|
|
18
|
+
INPUT_POINTS_SMALL = os.path.join(DATA_POINTS_PATH, "Points_virtuels_0382_6565.geojson")
|
|
19
|
+
OUTPUT_FILE_SMALL = os.path.join(TMP_PATH, "test_semis_2021_0382_6565_LA93_IGN69.laz")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def setup_module(module):
|
|
23
|
+
os.makedirs("test/data/output", exist_ok=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_get_tile_bbox():
|
|
27
|
+
bbox = add_points_in_pointcloud.get_tile_bbox(INPUT_FILE, 1000)
|
|
28
|
+
assert bbox == (292000.0, 6832000.0, 293000.0, 6833000.0) # check the bbox from LIDAR tile
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_clip_3d_points_to_tile():
|
|
32
|
+
points_clipped = add_points_in_pointcloud.clip_3d_points_to_tile(INPUT_POINTS, INPUT_FILE, "EPSG:2154")
|
|
33
|
+
assert len(points_clipped) == 678 # chech the entity's number of points
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_add_line_to_lidar():
|
|
37
|
+
points_clipped = add_points_in_pointcloud.clip_3d_points_to_tile(INPUT_POINTS, INPUT_FILE, "EPSG:2154")
|
|
38
|
+
|
|
39
|
+
add_points_in_pointcloud.add_points_to_las(points_clipped, INPUT_FILE, OUTPUT_FILE, 68)
|
|
40
|
+
assert Path(OUTPUT_FILE).exists() # check output exists
|
|
41
|
+
|
|
42
|
+
# Filter pointcloud by classes
|
|
43
|
+
pipeline = (
|
|
44
|
+
pdal.Reader.las(filename=OUTPUT_FILE, nosrs=True)
|
|
45
|
+
| pdal.Filter.range(
|
|
46
|
+
limits="Classification[68:68]",
|
|
47
|
+
)
|
|
48
|
+
| pdal.Filter.stats()
|
|
49
|
+
)
|
|
50
|
+
pipeline.execute()
|
|
51
|
+
metadata = pipeline.metadata
|
|
52
|
+
# Count the pointcloud's number from classe "68"
|
|
53
|
+
point_count = metadata["metadata"]["filters.stats"]["statistic"][0]["count"]
|
|
54
|
+
assert point_count == 678
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_get_tile_bbox_small():
|
|
58
|
+
# Tile is not complete (NOT 1km * 1km)
|
|
59
|
+
bbox = add_points_in_pointcloud.get_tile_bbox(INPUT_FILE_SMALL, 1000)
|
|
60
|
+
assert bbox == (382000.0, 6564000.0, 383000.0, 6565000.0) # return BBOX 1km * 1km
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_add_line_to_lidar_small():
|
|
64
|
+
# Tile is not complete (NOT 1km * 1km)
|
|
65
|
+
points_clipped = add_points_in_pointcloud.clip_3d_points_to_tile(INPUT_POINTS_SMALL, INPUT_FILE_SMALL, "EPSG:2154")
|
|
66
|
+
|
|
67
|
+
add_points_in_pointcloud.add_points_to_las(points_clipped, INPUT_FILE_SMALL, OUTPUT_FILE_SMALL, 68)
|
|
68
|
+
assert Path(OUTPUT_FILE).exists() # check output exists
|
|
69
|
+
|
|
70
|
+
# Filter pointcloud by classes
|
|
71
|
+
pipeline = (
|
|
72
|
+
pdal.Reader.las(filename=OUTPUT_FILE_SMALL, nosrs=True)
|
|
73
|
+
| pdal.Filter.range(
|
|
74
|
+
limits="Classification[68:68]",
|
|
75
|
+
)
|
|
76
|
+
| pdal.Filter.stats()
|
|
77
|
+
)
|
|
78
|
+
pipeline.execute()
|
|
79
|
+
metadata = pipeline.metadata
|
|
80
|
+
# Count the pointcloud's number from classe "68"
|
|
81
|
+
point_count = metadata["metadata"]["filters.stats"]["statistic"][0]["count"]
|
|
82
|
+
assert point_count == 186
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|