pointtorch 0.1.0__py3-none-any.whl

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 (36) hide show
  1. pointtorch/__init__.py +3 -0
  2. pointtorch/config/__init__.py +5 -0
  3. pointtorch/config/_optional_dependencies.py +35 -0
  4. pointtorch/core/__init__.py +6 -0
  5. pointtorch/core/_point_cloud.py +149 -0
  6. pointtorch/core/_read.py +35 -0
  7. pointtorch/io/__init__.py +13 -0
  8. pointtorch/io/_base_point_cloud_reader.py +104 -0
  9. pointtorch/io/_base_point_cloud_writer.py +99 -0
  10. pointtorch/io/_csv_reader.py +84 -0
  11. pointtorch/io/_csv_writer.py +80 -0
  12. pointtorch/io/_hdf_reader.py +87 -0
  13. pointtorch/io/_hdf_writer.py +85 -0
  14. pointtorch/io/_las_reader.py +104 -0
  15. pointtorch/io/_las_writer.py +145 -0
  16. pointtorch/io/_point_cloud_io_data.py +19 -0
  17. pointtorch/io/_point_cloud_reader.py +51 -0
  18. pointtorch/io/_point_cloud_writer.py +52 -0
  19. pointtorch/operations/__init__.py +0 -0
  20. pointtorch/operations/numpy/__init__.py +9 -0
  21. pointtorch/operations/numpy/_make_labels_consecutive.py +64 -0
  22. pointtorch/operations/numpy/_voxel_downsampling.py +114 -0
  23. pointtorch/operations/torch/__init__.py +14 -0
  24. pointtorch/operations/torch/_knn_search.py +443 -0
  25. pointtorch/operations/torch/_make_labels_consecutive.py +63 -0
  26. pointtorch/operations/torch/_neighbor_search.py +122 -0
  27. pointtorch/operations/torch/_pack_batch.py +61 -0
  28. pointtorch/operations/torch/_radius_search.py +605 -0
  29. pointtorch/operations/torch/_ravel_index.py +119 -0
  30. pointtorch/operations/torch/_voxel_downsampling.py +135 -0
  31. pointtorch/py.typed +0 -0
  32. pointtorch-0.1.0.dist-info/LICENSE +21 -0
  33. pointtorch-0.1.0.dist-info/METADATA +94 -0
  34. pointtorch-0.1.0.dist-info/RECORD +36 -0
  35. pointtorch-0.1.0.dist-info/WHEEL +5 -0
  36. pointtorch-0.1.0.dist-info/top_level.txt +1 -0
pointtorch/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ Classes and methods that are available in the top-level namespace. """
2
+
3
+ from pointtorch.core import *
@@ -0,0 +1,5 @@
1
+ """ Utilities for configuring the package setup. """
2
+
3
+ from ._optional_dependencies import *
4
+
5
+ __all__ = [name for name in globals().keys() if not name.startswith("_")]
@@ -0,0 +1,35 @@
1
+ """ Utilities for configuring the package setup. """
2
+
3
+ __all__ = ["open3d_is_available", "pytorch3d_is_available"]
4
+
5
+
6
+ def open3d_is_available():
7
+ """
8
+ Returns: `True` if Open3D is installed and `False` otherwise.
9
+ """
10
+
11
+ open3d_available = False
12
+ try:
13
+ from open3d.ml import torch as _ # pylint: disable=import-outside-toplevel
14
+
15
+ open3d_available = True
16
+ except (ModuleNotFoundError, TypeError, Exception): # pylint: disable=broad-exception-caught
17
+ pass
18
+
19
+ return open3d_available
20
+
21
+
22
+ def pytorch3d_is_available() -> bool:
23
+ """
24
+ Returns: `True` if PyTorch3D is installed and `False` otherwise.
25
+ """
26
+
27
+ pytorch3d_available = False
28
+ try:
29
+ import pytorch3d as _ # pylint: disable=import-outside-toplevel
30
+
31
+ pytorch3d_available = True
32
+ except (ModuleNotFoundError, TypeError):
33
+ pass
34
+
35
+ return pytorch3d_available
@@ -0,0 +1,6 @@
1
+ """ Core data structures for point cloud processing. """
2
+
3
+ from ._point_cloud import *
4
+ from ._read import *
5
+
6
+ __all__ = [name for name in globals().keys() if not name.startswith("_")]
@@ -0,0 +1,149 @@
1
+ """ Point cloud object. """
2
+
3
+ __all__ = ["PointCloud", "PointCloudSeries"]
4
+
5
+ from pathlib import Path
6
+ from typing import Dict, Hashable, Iterable, List, Optional, Union
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import pandas as pd
11
+
12
+ from pointtorch.io import PointCloudIoData, PointCloudWriter
13
+
14
+
15
+ class PointCloud(pd.DataFrame):
16
+ """Point cloud object. Subclass of
17
+ `pd.DataFrame <https://pd.pydata.org/docs/reference/api/pd.DataFrame.html>`_
18
+
19
+ Args:
20
+ identifier: Point cloud identifier. Defaults to `None`.
21
+ x_max_resolution: Maximum resolution of the point cloud's x-coordinates in meter. Defaults to `None`.
22
+ y_max_resolution: Maximum resolution of the point cloud's y-coordinates in meter. Defaults to `None`.
23
+ z_max_resolution: Maximum resolution of the point cloud's z-coordinates in meter. Defaults to `None`.
24
+
25
+ For a documentation of other parameters, see the documentation of \
26
+ `pd.DataFrame <https://pd.pydata.org/docs/reference/api/pd.DataFrame.html>`_.
27
+
28
+ Attributes:
29
+ identifier: Point cloud identifier.
30
+ x_max_resolution: Maximum resolution of the point cloud's x-coordinates in meter.
31
+ y_max_resolution: Maximum resolution of the point cloud's y-coordinates in meter.
32
+ z_max_resolution: Maximum resolution of the point cloud's z-coordinates in meter.
33
+ """
34
+
35
+ _metadata = ["identifier", "x_max_resolution", "y_max_resolution", "z_max_resolution"]
36
+
37
+ def __init__( # pylint: disable=too-many-positional-arguments
38
+ self,
39
+ data: Union[npt.ArrayLike, Iterable, Dict, pd.DataFrame],
40
+ index: Optional[Union[pd.Index, npt.NDArray[np.int64]]] = None,
41
+ columns: Optional[Union[pd.Index, npt.ArrayLike, List[str]]] = None,
42
+ dtype: Optional[np.dtype] = None,
43
+ copy: Optional[bool] = True,
44
+ identifier: Optional[str] = None,
45
+ x_max_resolution: Optional[float] = None,
46
+ y_max_resolution: Optional[float] = None,
47
+ z_max_resolution: Optional[float] = None,
48
+ ) -> None:
49
+ super().__init__(data=data, index=index, columns=columns, dtype=dtype, copy=copy) # type: ignore[call-arg]
50
+ self.identifier = identifier
51
+ self.x_max_resolution = x_max_resolution
52
+ self.y_max_resolution = y_max_resolution
53
+ self.z_max_resolution = z_max_resolution
54
+
55
+ @property
56
+ def _constructor(self):
57
+ return PointCloud
58
+
59
+ @property
60
+ def _constructor_sliced(self):
61
+ return PointCloudSeries
62
+
63
+ def xyz(self) -> npt.NDArray[np.float64]:
64
+ """
65
+ Returns:
66
+ x, y, and z coordinates of the points in the point cloud.
67
+
68
+ Raises:
69
+ RuntimeError: if "x", "y", or "z" are not in `self.columns`.
70
+ """
71
+
72
+ if "x" not in self.columns or "y" not in self.columns or "z" not in self.columns:
73
+ raise RuntimeError("The point cloud does not contain xyz coordinates.")
74
+
75
+ return self[["x", "y", "z"]].astype(np.float64).to_numpy()
76
+
77
+ def to(self, file_path: Union[str, Path], columns: Optional[List[str]] = None) -> None:
78
+ """
79
+ Writes the point cloud to a file.
80
+
81
+ Args:
82
+ file_path: Path of the output file.
83
+ columns: Point cloud columns to be written. The x, y, and z columns are always written.
84
+
85
+ Raises:
86
+ ValueError: If the point cloud format is not supported by the writer or if `columns` contains a column name
87
+ that is not existing in the point cloud.
88
+ """
89
+
90
+ writer = PointCloudWriter()
91
+ point_cloud_data = PointCloudIoData(
92
+ self,
93
+ identifier=self.identifier,
94
+ x_max_resolution=self.x_max_resolution,
95
+ y_max_resolution=self.y_max_resolution,
96
+ z_max_resolution=self.z_max_resolution,
97
+ )
98
+ writer.write(point_cloud_data, file_path, columns=columns)
99
+
100
+
101
+ class PointCloudSeries(pd.Series):
102
+ """
103
+ A data series that represents a point cloud column. Subclass of
104
+ `pd.Series <https://pd.pydata.org/pandas-docs/stable/reference/api/pd.Series.html>`_.
105
+
106
+ Args:
107
+ identifier: Point cloud identifier. Defaults to `None`.
108
+ x_max_resolution: Maximum resolution of the point cloud's x-coordinates in meter. Defaults to `None`.
109
+ y_max_resolution: Maximum resolution of the point cloud's y-coordinates in meter. Defaults to `None`.
110
+ z_max_resolution: Maximum resolution of the point cloud's z-coordinates in meter. Defaults to `None`.
111
+
112
+ For a documentation of other parameters, see the documentation of \
113
+ `pd.Series <https://pd.pydata.org/pandas-docs/stable/reference/api/pd.Series.html>`_.
114
+
115
+
116
+ Attributes:
117
+ identifier: Point cloud identifier.
118
+ x_max_resolution: Maximum resolution of the point cloud's x-coordinates in meter.
119
+ y_max_resolution: Maximum resolution of the point cloud's y-coordinates in meter.
120
+ z_max_resolution: Maximum resolution of the point cloud's z-coordinates in meter.
121
+ """
122
+
123
+ _metadata = ["identifier", "x_max_resolution", "y_max_resolution", "z_max_resolution"]
124
+
125
+ def __init__( # pylint: disable=too-many-positional-arguments
126
+ self,
127
+ data: Optional[Union[npt.ArrayLike, Iterable, Dict, int, float, str]] = None,
128
+ index: Optional[Union[pd.Index, npt.NDArray[np.int64]]] = None,
129
+ dtype: Optional[Union[str, np.dtype, pd.api.extensions.ExtensionDtype]] = None,
130
+ name: Optional[Hashable] = None,
131
+ copy: Optional[bool] = True,
132
+ identifier: Optional[str] = None,
133
+ x_max_resolution: Optional[float] = None,
134
+ y_max_resolution: Optional[float] = None,
135
+ z_max_resolution: Optional[float] = None,
136
+ ) -> None:
137
+ super().__init__(data=data, index=index, dtype=dtype, name=name, copy=copy) # type: ignore[call-arg]
138
+ self.identifier = identifier
139
+ self.x_max_resolution = x_max_resolution
140
+ self.y_max_resolution = y_max_resolution
141
+ self.z_max_resolution = z_max_resolution
142
+
143
+ @property
144
+ def _constructor(self):
145
+ return PointCloudSeries
146
+
147
+ @property
148
+ def _constructor_expanddim(self):
149
+ return PointCloud
@@ -0,0 +1,35 @@
1
+ """ Method for reading point cloud files. """
2
+
3
+ import pathlib
4
+ from typing import List, Optional, Union
5
+
6
+ from pointtorch.core._point_cloud import PointCloud
7
+ from pointtorch.io import PointCloudReader
8
+
9
+
10
+ def read(file_path: Union[str, pathlib.Path], columns: Optional[List[str]] = None) -> PointCloud:
11
+ """
12
+ Method for reading point cloud files.
13
+
14
+ Args:
15
+ file_path: Path of the point cloud file to be read.
16
+ columns: Name of the point cloud columns to be read. The x, y, and z columns are always read.
17
+
18
+ Returns:
19
+ Point cloud object.
20
+
21
+ Raises:
22
+ ValueError: If the point cloud format is not supported by the reader.
23
+ """
24
+
25
+ reader = PointCloudReader()
26
+ point_cloud_data = reader.read(file_path, columns=columns)
27
+ point_cloud = PointCloud(
28
+ point_cloud_data.data,
29
+ identifier=point_cloud_data.identifier,
30
+ x_max_resolution=point_cloud_data.x_max_resolution,
31
+ y_max_resolution=point_cloud_data.y_max_resolution,
32
+ z_max_resolution=point_cloud_data.z_max_resolution,
33
+ )
34
+
35
+ return point_cloud
@@ -0,0 +1,13 @@
1
+ """ Tools for reading and writing point cloud files. """
2
+
3
+ from ._csv_reader import *
4
+ from ._csv_writer import *
5
+ from ._hdf_reader import *
6
+ from ._hdf_writer import *
7
+ from ._las_reader import *
8
+ from ._las_writer import *
9
+ from ._point_cloud_io_data import *
10
+ from ._point_cloud_reader import *
11
+ from ._point_cloud_writer import *
12
+
13
+ __all__ = [name for name in globals().keys() if not name.startswith("_")]
@@ -0,0 +1,104 @@
1
+ """ Abstract base class for implementing point cloud file readers. """
2
+
3
+ __all__ = ["BasePointCloudReader"]
4
+
5
+ import abc
6
+ import pathlib
7
+ from typing import List, Optional, Tuple, Union
8
+
9
+ import pandas as pd
10
+
11
+ from ._point_cloud_io_data import PointCloudIoData
12
+
13
+
14
+ class BasePointCloudReader(abc.ABC):
15
+ """Abstract base class for implementing point cloud file readers."""
16
+
17
+ @abc.abstractmethod
18
+ def supported_file_formats(self) -> List[str]:
19
+ """
20
+ Returns:
21
+ File formats supported by the point cloud file reader.
22
+ """
23
+
24
+ def read(self, file_path: Union[str, pathlib.Path], columns: Optional[List[str]] = None) -> PointCloudIoData:
25
+ """
26
+ Reads a point cloud file.
27
+
28
+ Args:
29
+ file_path: Path of the point cloud file to be read.
30
+ columns: Name of the point cloud columns to be read. The x, y, and z columns are always read.
31
+
32
+ Returns:
33
+ Point cloud object.
34
+
35
+ Raises:
36
+ ValueError: If the point cloud format is not supported by the reader.
37
+ """
38
+
39
+ if isinstance(file_path, str):
40
+ file_path = pathlib.Path(file_path)
41
+
42
+ file_format = file_path.suffix.lstrip(".")
43
+ if file_format not in self.supported_file_formats():
44
+ raise ValueError(f"The {file_format} format is not supported by the point cloud reader.")
45
+
46
+ identifier = self._read_identifier(file_path)
47
+ file_id = file_path.stem if identifier is None else identifier
48
+
49
+ if columns is not None:
50
+ columns = columns.copy()
51
+
52
+ # The x, y, z coordinates are always loaded.
53
+ for idx, coord in enumerate(["x", "y", "z"]):
54
+ if coord not in columns:
55
+ columns.insert(idx, coord)
56
+
57
+ point_cloud_df = self._read_points(file_path, columns=columns)
58
+ (x_max_resolution, y_max_resolution, z_max_resolution) = self._read_max_resolutions(file_path)
59
+
60
+ return PointCloudIoData(
61
+ point_cloud_df,
62
+ identifier=file_id,
63
+ x_max_resolution=x_max_resolution,
64
+ y_max_resolution=y_max_resolution,
65
+ z_max_resolution=z_max_resolution,
66
+ )
67
+
68
+ @abc.abstractmethod
69
+ def _read_points(self, file_path: pathlib.Path, columns: Optional[List[str]] = None) -> pd.DataFrame:
70
+ """
71
+ Reads point data from a point cloud file. This method has to be overriden by child classes.
72
+
73
+ Args:
74
+ file_path: Path of the point cloud file to be read.
75
+ columns: Name of the point cloud columns to be read.
76
+
77
+ Returns:
78
+ Point cloud data.
79
+ """
80
+
81
+ @staticmethod
82
+ @abc.abstractmethod
83
+ def _read_max_resolutions(file_path: pathlib.Path) -> Tuple[Optional[float], Optional[float], Optional[float]]:
84
+ """
85
+ Reads the maximum resolution for each coordinate dimension from the point cloud file.
86
+
87
+ Args:
88
+ file_path: Path of the point cloud file to be read.
89
+
90
+ Returns:
91
+ Maximum resolution of the x-, y-, and z-coordinates of the point cloud.
92
+ """
93
+
94
+ @staticmethod
95
+ def _read_identifier(file_path: pathlib.Path) -> Optional[str]: # pylint: disable=unused-argument
96
+ """
97
+ Reads the point cloud identifier from the point cloud file. Storing a file identifier is not supported by all
98
+ file formats and :code:`None` may be returned when no file identifier is stored in a file.
99
+
100
+ Returns:
101
+ Point cloud identifier.
102
+ """
103
+
104
+ return None
@@ -0,0 +1,99 @@
1
+ """ Abstract base class for implementing point cloud file writers. """
2
+
3
+ __all__ = ["BasePointCloudWriter"]
4
+
5
+ import abc
6
+ import pathlib
7
+ from typing import List, Optional, Union
8
+
9
+ import pandas as pd
10
+
11
+ from ._point_cloud_io_data import PointCloudIoData
12
+
13
+
14
+ class BasePointCloudWriter(abc.ABC):
15
+ """Abstract base class for implementing point cloud file writers."""
16
+
17
+ @abc.abstractmethod
18
+ def supported_file_formats(self) -> List[str]:
19
+ """
20
+ Returns:
21
+ File formats supported by the point cloud file writer.
22
+ """
23
+
24
+ def write(
25
+ self, point_cloud: PointCloudIoData, file_path: Union[str, pathlib.Path], columns: Optional[List[str]] = None
26
+ ) -> None:
27
+ """
28
+ Writes a point cloud to a file.
29
+
30
+ Args:
31
+ point_cloud: Point cloud to be written.
32
+ file_path: Path of the output file.
33
+ columns: Point cloud columns to be written. The x, y, and z columns are always written.
34
+
35
+ Raises:
36
+ ValueError: If the point cloud format is not supported by the writer or if `columns` contains a column name
37
+ that is not existing in the point cloud.
38
+ """
39
+
40
+ if isinstance(file_path, str):
41
+ file_path = pathlib.Path(file_path)
42
+
43
+ file_format = file_path.suffix.lstrip(".")
44
+ if file_format not in self.supported_file_formats():
45
+ raise ValueError(f"The {file_format} format is not supported by the point cloud writer.")
46
+
47
+ point_cloud_df = point_cloud.data
48
+ # pylint: disable=duplicate-code
49
+ if columns is not None:
50
+ columns = columns.copy()
51
+
52
+ # The x, y, z coordinates are always written.
53
+ for idx, coord in enumerate(["x", "y", "z"]):
54
+ if coord not in columns:
55
+ columns.insert(idx, coord)
56
+
57
+ missing_columns = set(columns).difference(set(point_cloud_df.columns))
58
+ if len(missing_columns) == 1:
59
+ raise ValueError(f"The point cloud does not contain a column named {list(missing_columns)[0]}.")
60
+ if len(missing_columns) > 1:
61
+ missing_columns_list = list(missing_columns)
62
+ missing_columns_str = f"{', '.join(missing_columns_list[:-1])}, and {missing_columns_list[-1]}"
63
+ raise ValueError(f"The point cloud does not contain columns named {missing_columns_str}.")
64
+ point_cloud_df = point_cloud_df[columns]
65
+
66
+ self._write_data(
67
+ point_cloud_df,
68
+ file_path,
69
+ identifier=point_cloud.identifier,
70
+ x_max_resolution=point_cloud.x_max_resolution,
71
+ y_max_resolution=point_cloud.y_max_resolution,
72
+ z_max_resolution=point_cloud.z_max_resolution,
73
+ )
74
+
75
+ @abc.abstractmethod
76
+ def _write_data(
77
+ self,
78
+ point_cloud: pd.DataFrame,
79
+ file_path: pathlib.Path,
80
+ *,
81
+ identifier: Optional[str] = None,
82
+ x_max_resolution: Optional[float] = None,
83
+ y_max_resolution: Optional[float] = None,
84
+ z_max_resolution: Optional[float] = None,
85
+ ) -> None:
86
+ """
87
+ Writes a point cloud to a file. This method has to be overriden by child classes.
88
+
89
+ Args:
90
+ point_cloud: Point cloud to be written.
91
+ file_path: Path of the output file.
92
+ identifier: Identifier of the point cloud.
93
+ x_max_resolution (float, optional): Maximum resolution of the point cloud's x-coordinates in meter. Defaults
94
+ to `None`.
95
+ y_max_resolution (float, optional): Maximum resolution of the point cloud's y-coordinates in meter. Defaults
96
+ to `None`.
97
+ z_max_resolution (float, optional): Maximum resolution of the point cloud's z-coordinates in meter. Defaults
98
+ to `None`.
99
+ """
@@ -0,0 +1,84 @@
1
+ """ Point cloud file reader for csv and txt files. """
2
+
3
+ __all__ = ["CsvReader"]
4
+
5
+ import pathlib
6
+ from typing import List, Optional, Tuple, Union
7
+
8
+ import pandas as pd
9
+
10
+ from ._base_point_cloud_reader import BasePointCloudReader
11
+ from ._point_cloud_io_data import PointCloudIoData
12
+
13
+
14
+ class CsvReader(BasePointCloudReader):
15
+ """Point cloud file reader for csv and txt files."""
16
+
17
+ def supported_file_formats(self) -> List[str]:
18
+ """
19
+ Returns:
20
+ File formats supported by the point cloud file reader.
21
+ """
22
+
23
+ return ["csv", "txt"]
24
+
25
+ def read(self, file_path: Union[str, pathlib.Path], columns: Optional[List[str]] = None) -> PointCloudIoData:
26
+ """
27
+ Reads a point cloud file.
28
+
29
+ Args:
30
+ file_path: Path of the point cloud file to be read.
31
+ columns: Name of the point cloud columns to be read. The x, y, and z columns are always read.
32
+
33
+ Returns:
34
+ Point cloud object.
35
+
36
+ Raises:
37
+ ValueError: If the point cloud format is not supported by the reader.
38
+ """
39
+ # The method from the base is called explicitly so that the read method appears in the documentation of this
40
+ # class.
41
+ return super().read(file_path, columns=columns)
42
+
43
+ def _read_points(self, file_path: pathlib.Path, columns: Optional[List[str]] = None) -> pd.DataFrame:
44
+ """
45
+ Reads point data from a point cloud file in csv or txt format.
46
+
47
+ Args:
48
+ file_path: Path of the point cloud file to be read.
49
+ columns: Name of the point cloud columns to be read. The x, y, and z columns are always read.
50
+
51
+ Returns:
52
+ Point cloud data.
53
+ """
54
+
55
+ file_format = file_path.suffix.lstrip(".")
56
+ return pd.read_csv(file_path, usecols=columns, sep="," if file_format == "csv" else " ")
57
+
58
+ @staticmethod
59
+ def _read_max_resolutions(file_path: pathlib.Path) -> Tuple[float, float, float]:
60
+ """
61
+ Reads the maximum resolution for each coordinate dimension from the point cloud file.
62
+
63
+ Args:
64
+ file_path: Path of the point cloud file to be read.
65
+
66
+ Returns:
67
+ Maximum resolution of the x-, y-, and z-coordinates of the point cloud.
68
+ """
69
+
70
+ file_format = file_path.suffix.lstrip(".")
71
+ df = pd.read_csv(file_path, usecols=["x", "y", "z"], sep="," if file_format == "csv" else " ", dtype=str)
72
+
73
+ # The precision of each coordinate is calculated by counting the digits after the decimal.
74
+ x_max_resolution = (
75
+ df["x"].str.split(".").apply(lambda x: round(0.1 ** len(x[1]), len(x)) if len(x) > 1 else 1).min()
76
+ )
77
+ y_max_resolution = (
78
+ df["y"].str.split(".").apply(lambda x: round(0.1 ** len(x[1]), len(x)) if len(x) > 1 else 1).min()
79
+ )
80
+ z_max_resolution = (
81
+ df["z"].str.split(".").apply(lambda x: round(0.1 ** len(x[1]), len(x)) if len(x) > 1 else 1).min()
82
+ )
83
+
84
+ return x_max_resolution, y_max_resolution, z_max_resolution
@@ -0,0 +1,80 @@
1
+ """ Point cloud file writer for csv and txt files. """
2
+
3
+ __all__ = ["CsvWriter"]
4
+
5
+ import math
6
+ import pathlib
7
+ from typing import List, Optional, Union
8
+
9
+ import pandas as pd
10
+
11
+ from ._base_point_cloud_writer import BasePointCloudWriter
12
+ from ._point_cloud_io_data import PointCloudIoData
13
+
14
+
15
+ class CsvWriter(BasePointCloudWriter):
16
+ """Point cloud file writer for csv and txt files."""
17
+
18
+ def supported_file_formats(self) -> List[str]:
19
+ """
20
+ Returns:
21
+ File formats supported by the point cloud file writer.
22
+ """
23
+
24
+ return ["csv", "txt"]
25
+
26
+ def write(
27
+ self, point_cloud: PointCloudIoData, file_path: Union[str, pathlib.Path], columns: Optional[List[str]] = None
28
+ ) -> None:
29
+ """
30
+ Writes a point cloud to a file.
31
+
32
+ Args:
33
+ point_cloud: Point cloud to be written.
34
+ file_path: Path of the output file.
35
+ columns: Point cloud columns to be written. The x, y, and z columns are always written.
36
+
37
+ Raises:
38
+ ValueError: If the point cloud format is not supported by the writer or if `columns` contains a column name
39
+ that is not existing in the point cloud.
40
+ """
41
+ # The method from the base is called explicitly so that the read method appears in the documentation of this
42
+ # class.
43
+ super().write(point_cloud, file_path, columns=columns)
44
+
45
+ def _write_data(
46
+ self,
47
+ point_cloud: pd.DataFrame,
48
+ file_path: pathlib.Path,
49
+ *,
50
+ identifier: Optional[str] = None,
51
+ x_max_resolution: Optional[float] = None,
52
+ y_max_resolution: Optional[float] = None,
53
+ z_max_resolution: Optional[float] = None,
54
+ ) -> None:
55
+ """
56
+ Writes a point cloud to a file.
57
+
58
+ Args:
59
+ point_cloud: Point cloud to be written.
60
+ file_path: Path of the output file.
61
+ x_max_resolution: Maximum resolution of the point cloud's x-coordinates in meter. Defaults to `None`.
62
+ y_max_resolution: Maximum resolution of the point cloud's y-coordinates in meter. Defaults to `None`.
63
+ z_max_resolution: Maximum resolution of the point cloud's z-coordinates in meter. Defaults to `None`.
64
+ """
65
+
66
+ num_decimals = None
67
+
68
+ if x_max_resolution is not None:
69
+ num_decimals = math.ceil(-1 * min(0, math.log(x_max_resolution, 10)))
70
+ if y_max_resolution is not None:
71
+ num_decimals_y = math.ceil(-1 * min(0, math.log(y_max_resolution, 10)))
72
+ num_decimals = max(num_decimals, num_decimals_y) if num_decimals is not None else num_decimals_y
73
+ if z_max_resolution is not None:
74
+ num_decimals_z = math.ceil(-1 * min(0, math.log(z_max_resolution, 10)))
75
+ num_decimals = max(num_decimals, num_decimals_z) if num_decimals is not None else num_decimals_z
76
+
77
+ float_format = f"%.{num_decimals}f" if num_decimals is not None else None
78
+
79
+ file_format = file_path.suffix.lstrip(".")
80
+ point_cloud.to_csv(file_path, sep="," if file_format == "csv" else " ", index=False, float_format=float_format)