Skip to content

Commit 4ee94c9

Browse files
committed
rename dimension
1 parent 3ab93e5 commit 4ee94c9

File tree

6 files changed

+260
-8
lines changed

6 files changed

+260
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
- custom PDAL: fix CI for cicd_full (build docker image with custom PDAL, and skip custom PDAL test for local pytest)
2+
- las_rename_dimension: new tool to rename one or many dimensions
23

34
# 1.9.1
45
- las_add_points_to_pointcloud: Fix add points to LAS (use PDAL instead of Laspy)

pdaltools/count_occurences/count_occurences_for_attribute.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Count occurences of each value of a given attribute in a set of pointclouds.
2-
Eg. to count points of each class in classified point clouds """
2+
Eg. to count points of each class in classified point clouds"""
33

44
import argparse
55
import json

pdaltools/las_remove_dimensions.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,36 @@
55
from pdaltools.las_info import get_writer_parameters_from_reader_metadata
66

77

8-
def remove_dimensions_from_las(input_las: str, dimensions: [str], output_las: str):
8+
def remove_dimensions_from_points(points, metadata, dimensions: [str], output_las: str):
99
"""
1010
export new las without some dimensions
1111
"""
12-
pipeline = pdal.Pipeline() | pdal.Reader.las(input_las)
13-
pipeline.execute()
14-
points = pipeline.arrays[0]
12+
13+
mandatory_dimensions = ["X", "Y", "Z", "x", "y", "z"]
14+
output_dimensions_test = [dim for dim in dimensions if dim not in mandatory_dimensions]
15+
assert len(output_dimensions_test) == len(
16+
dimensions
17+
), "All dimensions to remove must not be mandatory dimensions (X,Y,Z,x,y,z)"
18+
1519
input_dimensions = list(points.dtype.fields.keys())
1620
output_dimensions = [dim for dim in input_dimensions if dim not in dimensions]
1721
points_pruned = points[output_dimensions]
18-
params = get_writer_parameters_from_reader_metadata(pipeline.metadata)
22+
params = get_writer_parameters_from_reader_metadata(metadata)
1923
pipeline_end = pdal.Pipeline(arrays=[points_pruned])
2024
pipeline_end |= pdal.Writer.las(output_las, forward="all", **params)
2125
pipeline_end.execute()
2226

2327

28+
def remove_dimensions_from_las(input_las: str, dimensions: [str], output_las: str):
29+
"""
30+
export new las without some dimensions
31+
"""
32+
pipeline = pdal.Pipeline() | pdal.Reader.las(input_las)
33+
pipeline.execute()
34+
points = pipeline.arrays[0]
35+
remove_dimensions_from_points(points, pipeline.metadata, dimensions, output_las)
36+
37+
2438
def parse_args():
2539
parser = argparse.ArgumentParser("Remove dimensions from las")
2640
parser.add_argument(

pdaltools/las_rename_dimension.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Rename dimensions in a LAS file using PDAL's Python API.
3+
4+
This script allows renaming dimensions in a LAS file while preserving all other data.
5+
"""
6+
7+
import argparse
8+
import pdal
9+
import sys
10+
from pathlib import Path
11+
from pdaltools.las_remove_dimensions import remove_dimensions_from_points
12+
13+
14+
def rename_dimension(input_file: str, output_file: str, old_dims: list[str], new_dims: list[str]):
15+
"""
16+
Rename one or multiple dimensions in a LAS file using PDAL.
17+
18+
Args:
19+
input_file: Path to the input LAS file
20+
output_file: Path to save the output LAS file
21+
old_dims: List of names of dimensions to rename
22+
new_dims: List of new names for the dimensions
23+
"""
24+
25+
# Validate dimensions
26+
if len(old_dims) != len(new_dims):
27+
raise ValueError("Number of old dimensions must match number of new dimensions")
28+
29+
mandatory_dimensions = ['X', 'Y', 'Z', 'x', 'y', 'z']
30+
for dim in new_dims:
31+
if dim in mandatory_dimensions:
32+
raise ValueError(f"New dimension {dim} cannot be a mandatory dimension (X,Y,Z,x,y,z)")
33+
34+
pipeline = pdal.Pipeline() | pdal.Reader.las(input_file)
35+
for old, new in zip(old_dims, new_dims):
36+
pipeline |= pdal.Filter.ferry(dimensions=f"{old} => {new}")
37+
pipeline |= pdal.Writer.las(output_file)
38+
pipeline.execute()
39+
points = pipeline.arrays[0]
40+
41+
# Remove old dimensions
42+
remove_dimensions_from_points(points, pipeline.metadata, old_dims, output_file)
43+
44+
45+
def main():
46+
parser = argparse.ArgumentParser(description="Rename dimensions in a LAS file")
47+
parser.add_argument("input_file", help="Input LAS file")
48+
parser.add_argument("output_file", help="Output LAS file")
49+
parser.add_argument(
50+
"--old-dims",
51+
nargs="+",
52+
required=True,
53+
help="Names of dimensions to rename (can specify multiple)",
54+
)
55+
parser.add_argument(
56+
"--new-dims",
57+
nargs="+",
58+
required=True,
59+
help="New names for the dimensions (must match --old-dims count)",
60+
)
61+
62+
args = parser.parse_args()
63+
64+
# Validate input file
65+
input_path = Path(args.input_file)
66+
if not input_path.exists():
67+
print(f"Error: Input file {args.input_file} does not exist", file=sys.stderr)
68+
sys.exit(1)
69+
70+
# Validate output file
71+
output_path = Path(args.output_file)
72+
if output_path.exists():
73+
print(f"Warning: Output file {args.output_file} already exists. It will be overwritten.")
74+
75+
rename_dimension(args.input_file, args.output_file, args.old_dims, args.new_dims)
76+
77+
78+
if __name__ == "__main__":
79+
main()

pdaltools/unlock_file.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""Tools to handle malformed las/laz files
2-
"""
1+
"""Tools to handle malformed las/laz files"""
32

43
# https://gis.stackexchange.com/questions/413191/python-pdal-error-reading-format-1-4-las-file-readers-las-error-global-enco
54

test/test_las_rename_dimension.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import os
2+
import pytest
3+
import tempfile
4+
import numpy as np
5+
import laspy
6+
import sys
7+
from pdaltools.las_rename_dimension import rename_dimension, main
8+
from pyproj import CRS
9+
10+
def create_test_las_file():
11+
"""Create a temporary LAS file with test data."""
12+
with tempfile.NamedTemporaryFile(suffix='.las', delete=False) as tmp_file:
13+
# Create a LAS file with some test points
14+
header = laspy.LasHeader(point_format=3, version="1.4")
15+
header.add_extra_dim(laspy.ExtraBytesParams(name="test_dim", type=np.float32))
16+
header.add_extra_dim(laspy.ExtraBytesParams(name="test_dim2", type=np.int32))
17+
18+
las = laspy.LasData(header)
19+
20+
crs_pyproj = CRS.from_string("epsg:4326")
21+
las.header.add_crs(crs_pyproj)
22+
23+
# Add some test points
24+
las.x = np.array([1.0, 2.0, 3.0])
25+
las.y = np.array([4.0, 5.0, 6.0])
26+
las.z = np.array([7.0, 8.0, 9.0])
27+
las.test_dim = np.array([10.0, 11.0, 12.0])
28+
las.test_dim2 = np.array([12, 13, 14])
29+
30+
las.write(tmp_file.name)
31+
return tmp_file.name
32+
33+
def test_rename_dimension():
34+
"""Test renaming a dimension in a LAS file."""
35+
# Create a temporary input LAS file
36+
input_file = create_test_las_file()
37+
38+
# Create temporary output file
39+
with tempfile.NamedTemporaryFile(suffix='.las', delete=False) as tmp_file:
40+
output_file = tmp_file.name
41+
42+
try:
43+
# Rename dimension using direct function call
44+
rename_dimension(input_file, output_file, ["test_dim", "test_dim2"], ["new_test_dim", "new_test_dim2"])
45+
46+
# Verify the dimension was renamed
47+
with laspy.open(output_file) as las_file:
48+
las = las_file.read()
49+
assert "new_test_dim" in las.point_format.dimension_names
50+
assert "test_dim" not in las.point_format.dimension_names
51+
assert "new_test_dim2" in las.point_format.dimension_names
52+
assert "test_dim2" not in las.point_format.dimension_names
53+
54+
# Verify the data is preserved
55+
np.testing.assert_array_equal(las.x, [1.0, 2.0, 3.0])
56+
np.testing.assert_array_equal(las.y, [4.0, 5.0, 6.0])
57+
np.testing.assert_array_equal(las.z, [7.0, 8.0, 9.0])
58+
np.testing.assert_array_equal(las["new_test_dim"], [10.0, 11.0, 12.0])
59+
np.testing.assert_array_equal(las["new_test_dim2"], [12, 13, 14])
60+
finally:
61+
# Clean up temporary files
62+
try:
63+
os.unlink(input_file)
64+
os.unlink(output_file)
65+
except:
66+
pass
67+
68+
def test_rename_nonexistent_dimension():
69+
"""Test attempting to rename a dimension that doesn't exist."""
70+
input_file = create_test_las_file()
71+
72+
with tempfile.NamedTemporaryFile(suffix='.las', delete=False) as tmp_file:
73+
output_file = tmp_file.name
74+
75+
try:
76+
with pytest.raises(RuntimeError):
77+
rename_dimension(input_file, output_file, ["nonexistent_dim"], ["new_dim"])
78+
finally:
79+
os.unlink(input_file)
80+
os.unlink(output_file)
81+
82+
def test_rename_to_existing_dimension():
83+
"""Test attempting to rename to an existing dimension."""
84+
input_file = create_test_las_file()
85+
86+
with tempfile.NamedTemporaryFile(suffix='.las', delete=False) as tmp_file:
87+
output_file = tmp_file.name
88+
89+
try:
90+
with pytest.raises(ValueError):
91+
rename_dimension(input_file, output_file, ["test_dim"], ["x"])
92+
finally:
93+
os.unlink(input_file)
94+
os.unlink(output_file)
95+
96+
def test_rename_dimension_case_sensitive():
97+
"""Test that dimension renaming is case-sensitive."""
98+
input_file = create_test_las_file()
99+
100+
with tempfile.NamedTemporaryFile(suffix='.las', delete=False) as tmp_file:
101+
output_file = tmp_file.name
102+
103+
try:
104+
with pytest.raises(RuntimeError):
105+
rename_dimension(input_file, output_file, ["TEST_DIM"], ["new_dim"])
106+
finally:
107+
os.unlink(input_file)
108+
os.unlink(output_file)
109+
110+
111+
def test_rename_dimension_main():
112+
"""Test renaming dimensions using the main() function."""
113+
# Create a temporary input LAS file
114+
input_file = create_test_las_file()
115+
116+
# Create temporary output file
117+
with tempfile.NamedTemporaryFile(suffix='.las', delete=False) as tmp_file:
118+
output_file = tmp_file.name
119+
120+
try:
121+
# Save original sys.argv
122+
original_argv = sys.argv
123+
124+
# Mock command-line arguments
125+
sys.argv = [
126+
"las_rename_dimension.py", # script name
127+
input_file,
128+
output_file,
129+
"--old-dims", "test_dim", "test_dim2",
130+
"--new-dims", "new_test_dim", "new_test_dim2"
131+
]
132+
133+
# Call main() function
134+
main()
135+
136+
# Restore original sys.argv
137+
sys.argv = original_argv
138+
139+
# Verify the dimension was renamed
140+
with laspy.open(output_file) as las_file:
141+
las = las_file.read()
142+
assert "new_test_dim" in las.point_format.dimension_names
143+
assert "test_dim" not in las.point_format.dimension_names
144+
assert "new_test_dim2" in las.point_format.dimension_names
145+
assert "test_dim2" not in las.point_format.dimension_names
146+
147+
# Verify the data is preserved
148+
np.testing.assert_array_equal(las.x, [1.0, 2.0, 3.0])
149+
np.testing.assert_array_equal(las.y, [4.0, 5.0, 6.0])
150+
np.testing.assert_array_equal(las.z, [7.0, 8.0, 9.0])
151+
np.testing.assert_array_equal(las["new_test_dim"], [10.0, 11.0, 12.0])
152+
np.testing.assert_array_equal(las["new_test_dim2"], [12, 13, 14])
153+
finally:
154+
# Clean up temporary files
155+
try:
156+
os.unlink(input_file)
157+
os.unlink(output_file)
158+
except:
159+
pass

0 commit comments

Comments
 (0)