pointclouds 0.0.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.
- pointclouds/__init__.py +20 -0
- pointclouds/classify/__init__.py +1 -0
- pointclouds/classify/ground.py +1 -0
- pointclouds/classify/trees.py +1 -0
- pointclouds/convert.py +35 -0
- pointclouds/io/__init__.py +1 -0
- pointclouds/io/_records.py +196 -0
- pointclouds/io/copc.py +570 -0
- pointclouds/io/ept.py +724 -0
- pointclouds/io/las.py +119 -0
- pointclouds/ops/__init__.py +1 -0
- pointclouds/ops/clip.py +99 -0
- pointclouds/ops/decimate.py +107 -0
- pointclouds/ops/filter.py +109 -0
- pointclouds/ops/grid.py +241 -0
- pointclouds/ops/merge.py +107 -0
- pointclouds/points.py +801 -0
- pointclouds-0.0.0.dist-info/METADATA +95 -0
- pointclouds-0.0.0.dist-info/RECORD +21 -0
- pointclouds-0.0.0.dist-info/WHEEL +4 -0
- pointclouds-0.0.0.dist-info/licenses/LICENSE.md +675 -0
pointclouds/ops/merge.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Concatenate several clouds into one. (Phase 2 - T-9).
|
|
2
|
+
|
|
3
|
+
:func:`merge_arrays` assembles the combined coordinate and attribute arrays and settles the CRS, then
|
|
4
|
+
:meth:`PointCloud.merge` builds the cloud from them. Keeping the assembly here, separate from construction,
|
|
5
|
+
avoids importing :class:`PointCloud` at module load. Pure numpy - no GDAL, no pyramids.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pyproj import CRS
|
|
17
|
+
|
|
18
|
+
from pointclouds.points import PointCloud
|
|
19
|
+
|
|
20
|
+
__all__ = ["merge_arrays"]
|
|
21
|
+
|
|
22
|
+
#: The attribute dimensions that are carried through a merge, alongside x/y/z.
|
|
23
|
+
_ATTRIBUTES = ("intensity", "classification", "return_number")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def merge_arrays(clouds: Sequence[PointCloud]) -> dict[str, Any]:
|
|
27
|
+
"""Concatenate the clouds' arrays and resolve a single CRS.
|
|
28
|
+
|
|
29
|
+
A zero-point cloud is a no-op participant: it contributes no rows and takes no part in resolving the CRS or
|
|
30
|
+
in deciding which attributes are present, so it never turns a valid merge into a conflict.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
clouds: the clouds to merge, in order; each is loaded if its records are not in memory yet.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A mapping of ``PointCloud`` constructor keyword to value: ``x``/``y``/``z``, the ``crs``, and each
|
|
37
|
+
attribute that every point-carrying cloud has. An attribute no cloud carries is omitted; the CRS is the
|
|
38
|
+
one shared by the point-carrying clouds, or ``None`` when none carry points.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: when ``clouds`` is empty, when the clouds disagree on CRS, or when an attribute is present
|
|
42
|
+
on some clouds but absent on others (so the concatenation would misalign).
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> import numpy as np
|
|
46
|
+
>>> from pointclouds import PointCloud
|
|
47
|
+
>>> a = PointCloud(x=np.zeros(2), y=np.zeros(2), z=np.zeros(2))
|
|
48
|
+
>>> b = PointCloud(x=np.ones(3), y=np.zeros(3), z=np.zeros(3))
|
|
49
|
+
>>> arrays = merge_arrays([a, b])
|
|
50
|
+
>>> arrays["x"].tolist()
|
|
51
|
+
[0.0, 0.0, 1.0, 1.0, 1.0]
|
|
52
|
+
>>> sorted(arrays)
|
|
53
|
+
['crs', 'x', 'y', 'z']
|
|
54
|
+
"""
|
|
55
|
+
clouds = list(clouds)
|
|
56
|
+
if not clouds:
|
|
57
|
+
raise ValueError("merge needs at least one cloud")
|
|
58
|
+
|
|
59
|
+
point_counts = [c.x.shape[0] for c in clouds] # also forces any lazy cloud to load
|
|
60
|
+
carrying = [c for c, count in zip(clouds, point_counts, strict=True) if count > 0]
|
|
61
|
+
merged: dict[str, Any] = {
|
|
62
|
+
"x": np.concatenate([c.x for c in clouds]),
|
|
63
|
+
"y": np.concatenate([c.y for c in clouds]),
|
|
64
|
+
"z": np.concatenate([c.z for c in clouds]),
|
|
65
|
+
# A zero-point cloud contributes no points, so its CRS does not take part either (matching the attribute
|
|
66
|
+
# scoping below): resolve across the clouds that actually carry points, or None when none do.
|
|
67
|
+
"crs": _resolve_crs(carrying),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for name in _ATTRIBUTES:
|
|
71
|
+
arrays = [getattr(c, name) for c in clouds]
|
|
72
|
+
# A zero-point cloud (e.g. one filtered to empty) contributes no rows, so it neither has nor needs the
|
|
73
|
+
# attribute: judge presence only among the clouds that actually carry points. Its size-0 array still
|
|
74
|
+
# concatenates harmlessly when the attribute is kept.
|
|
75
|
+
present = [values.size > 0 for values, count in zip(arrays, point_counts, strict=True) if count > 0]
|
|
76
|
+
if present and all(present):
|
|
77
|
+
merged[name] = np.concatenate(arrays)
|
|
78
|
+
elif any(present):
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"cannot merge: {name!r} is present on some clouds but not others; "
|
|
81
|
+
"give every cloud the dimension, or none"
|
|
82
|
+
)
|
|
83
|
+
return merged
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _resolve_crs(clouds: Sequence[PointCloud]) -> CRS | None:
|
|
87
|
+
"""Return the one CRS the clouds share, or raise when they disagree.
|
|
88
|
+
|
|
89
|
+
``None`` (no known SRS) is treated as a distinct value, so merging a cloud that has a CRS with one that does
|
|
90
|
+
not is a conflict rather than a silent guess. An empty sequence (e.g. only zero-point clouds) resolves to
|
|
91
|
+
``None``.
|
|
92
|
+
"""
|
|
93
|
+
distinct: list[CRS | None] = []
|
|
94
|
+
for crs in (c.crs for c in clouds):
|
|
95
|
+
if not any(_crs_equal(crs, seen) for seen in distinct):
|
|
96
|
+
distinct.append(crs)
|
|
97
|
+
if len(distinct) > 1:
|
|
98
|
+
shown = ", ".join("None" if c is None else c.to_string() for c in distinct)
|
|
99
|
+
raise ValueError(f"cannot merge clouds with conflicting CRSs: {{{shown}}}")
|
|
100
|
+
return distinct[0] if distinct else None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _crs_equal(a: CRS | None, b: CRS | None) -> bool:
|
|
104
|
+
"""Whether two CRSs are the same, counting ``None`` as equal only to ``None``."""
|
|
105
|
+
if a is None or b is None:
|
|
106
|
+
return a is None and b is None
|
|
107
|
+
return a == b
|