Skip to content

Commit 9ff13f7

Browse files
committed
export cli only from pointcloud supported formats in CLI
1 parent d3d841c commit 9ff13f7

3 files changed

Lines changed: 73 additions & 26 deletions

File tree

CHANGELOG.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_,
66
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.
77

8+
Unreleased
9+
-------------
10+
11+
Added
12+
~~~~~~
13+
- exporting of las file using laspy
14+
15+
Fixed
16+
~~~~~~
17+
- wrong helpt for pointcloudset convert CLI: the bag or mcap dir needs to come right after the command
18+
819
0.10.1- (2025-04-28)
920
-------------
1021
Changed

src/pointcloudset/io/dataset/commandline.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55

66
import click # needed for documentation
77
import typer
8-
from pyntcloud.io import TO_FILE
98
from rich.console import Console
109
from rosbags.highlevel import AnyReader
1110

1211
import pointcloudset
1312
from pointcloudset import Dataset
13+
from pointcloudset.io import POINTCLOUD_TO_FILE
1414

1515
app = typer.Typer()
1616
console = Console()
1717

18-
TO_FILE_PYNTCLOUD = list(TO_FILE.keys())
19-
TO_FILE_CLI = TO_FILE_PYNTCLOUD.append("POINTCLOUDSET")
18+
19+
TO_FILE_PYNTCLOUD = [k.lower() for k in POINTCLOUD_TO_FILE.keys()]
20+
TO_FILE_CLI = TO_FILE_PYNTCLOUD + ["pointcloudset"]
2021

2122

2223
@app.command()
@@ -35,16 +36,16 @@ def convert(
3536
Examples:
3637
3738
convert all ROS1 bag files in a directory
38-
$ pointcloudset convert -d converted .
39+
$ pointcloudset convert . -d converted
3940
4041
convert all frames of bagfile xyz.bag into csv files
41-
$ pointcloudset convert -o csv -d converted_csv xyz.bag
42+
$ pointcloudset convert xyz.bag -o csv -d converted_csv xyz.bag
4243
4344
convert a ROS2 directoy to a pointcloudset file
44-
$ pointcloudset convert -d converted something_ros2
45+
$ pointcloudset convert something_ros2 -d converted
4546
4647
convert the first 10 frames of a bag file int0las files
47-
$ pointcloudset convert -o las -d converted_las --start 1 --end 10 xyz.bag
48+
$ pointcloudset convert xyz.bag -o las -d converted_las --start 1 --end 10
4849
"""
4950
console.line()
5051
console.rule(f"pointcloudset {pointcloudset.__version__}")
@@ -66,7 +67,7 @@ def convert(
6667
folder_to_write=folder_to_write_path,
6768
)
6869
console.print(f"{Path(bagfile_path).name} converted to {folder_to_write_path}")
69-
elif output_format.upper() in TO_FILE_PYNTCLOUD:
70+
elif output_format.lower() in TO_FILE_CLI:
7071
_convert_bag2files(
7172
topic,
7273
start_frame_number,
@@ -145,7 +146,7 @@ def _gen_file_paths(file_name):
145146

146147
def _gen_folder(folder_to_write: str, ros_file_path: str, output_format: str) -> Path:
147148
"""Generate the folder to write the converted files to."""
148-
suffix = "_pointcloudset" if output_format == "POINTCLOUDSET" else ""
149+
suffix = "_pointcloudset" if output_format == "POINTCLOUDSET" else f"_{output_format.lower()}"
149150
folder_to_write_path = Path(folder_to_write).joinpath(Path(ros_file_path).stem + suffix)
150151
folder_to_write_path.mkdir(exist_ok=False, parents=True)
151152
return folder_to_write_path
@@ -179,11 +180,11 @@ def _convert_bag2files(
179180
if end_frame_number is None:
180181
end_frame_number = len(dataset)
181182
for frame in range(start_frame_number, end_frame_number):
182-
pyntcloud = dataset[frame].to_instance("PYNTCLOUD")
183+
frame_pc = dataset[frame]
183184
orig_file = Path(ros_file_path).stem
184185
filename = folder_to_write_path.joinpath(f"{orig_file}_{frame}.{output_format.lower()}")
185186
console.print(f"frame {frame} of {Path(ros_file_path).name} converted to {filename}")
186-
pyntcloud.to_file(filename.as_posix())
187+
frame_pc.to_file(filename)
187188

188189

189190
typer_click_object = typer.main.get_command(app)

src/pointcloudset/io/pointcloud/las.py

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,6 @@
1212
LAS_VERSION = "1.4"
1313
LAS_PRECISION = 0.0001 # m
1414

15-
_NUMPY2LAS = {
16-
"uint8": "u1",
17-
"int8": "i1",
18-
"uint16": "u2",
19-
"int16": "i2",
20-
"uint32": "u4",
21-
"int32": "i4",
22-
"uint64": "u8",
23-
"int64": "i8",
24-
"float32": "f4",
25-
"float64": "f8",
26-
}
27-
2815

2916
def _choose_scale_offset(axis: np.ndarray) -> tuple[float, float]:
3017
"""
@@ -46,6 +33,55 @@ def _choose_scale_offset(axis: np.ndarray) -> tuple[float, float]:
4633
return scale, offset
4734

4835

36+
def _best_las_type(arr: np.ndarray) -> str:
37+
"""
38+
Return the smallest LAS data-type code ('u1', 'i1', …, 'f8') that can
39+
represent *arr* loss-lessly.
40+
41+
LAS → NumPy mapping codes:
42+
u1/i1 : unsigned/signed 8-bit integer
43+
u2/i2 : unsigned/signed 16-bit integer
44+
u4/i4 : unsigned/signed 32-bit integer
45+
u8/i8 : unsigned/signed 64-bit integer
46+
f4/f8 : 32- / 64-bit float
47+
"""
48+
dt = arr.dtype
49+
50+
# Integers
51+
if dt.kind in {"u", "i"}:
52+
lo, hi = int(arr.min()), int(arr.max())
53+
if dt.kind == "u": # unsigned
54+
if hi <= np.iinfo(np.uint8).max:
55+
return "u1"
56+
if hi <= np.iinfo(np.uint16).max:
57+
return "u2"
58+
if hi <= np.iinfo(np.uint32).max:
59+
return "u4"
60+
return "u8"
61+
else: # signed
62+
if lo >= np.iinfo(np.int8).min and hi <= np.iinfo(np.int8).max:
63+
return "i1"
64+
if lo >= np.iinfo(np.int16).min and hi <= np.iinfo(np.int16).max:
65+
return "i2"
66+
if lo >= np.iinfo(np.int32).min and hi <= np.iinfo(np.int32).max:
67+
return "i4"
68+
return "i8"
69+
70+
# Floats
71+
if dt.kind == "f":
72+
# If nothing is lost when casting to f4, prefer it.
73+
if np.allclose(arr.astype(np.float32), arr, rtol=0, atol=0):
74+
return "f4"
75+
return "f8"
76+
77+
# Booleans / bit-fields
78+
if dt.kind == "b":
79+
return "u1" # 0 / 1
80+
81+
# default to 64-bit float
82+
return "f8"
83+
84+
4985
def write_las(pointcloud: "PointCloud", file_path: Path) -> None:
5086
"""
5187
Export to LAS/LAZ (point format 7). Coordinates are stored at *precision_hint* or the
@@ -80,8 +116,7 @@ def write_las(pointcloud: "PointCloud", file_path: Path) -> None:
80116
# LAS ExtraBytes for everything else
81117
extra = {c for c in df.columns if c.lower() not in builtin}
82118
for col in sorted(extra):
83-
las_type = _NUMPY2LAS.get(df[col].dtype.name, "f8")
119+
las_type = _best_las_type(df[col].to_numpy())
84120
las.add_extra_dim(laspy.ExtraBytesParams(name=col, type=las_type))
85121
las[col] = df[col].to_numpy()
86-
87122
las.write(Path(file_path))

0 commit comments

Comments
 (0)