Source code for dtcc_model.geometry.pointcloud
# Copyright(C) 2023 Anders Logg
# Licensed under the MIT License
from dataclasses import dataclass, field
from typing import Union
import numpy as np
from .geometry import Geometry
from .bounds import Bounds
from dtcc_model import dtcc_pb2 as proto
[docs]
@dataclass
class PointCloud(Geometry):
"""Represents a set of points in 3D.
Attributes
----------
points : np.ndarray
The points of the point cloud as (n,3) dimensional numpy array.
classification : np.ndarray
The classification of the points as (n,) dimensional numpy array.
intensity : np.ndarray
The intensity of the points as (n,) dimensional numpy array.
return_number : np.ndarray
The return number of the points as (n,) dimensional numpy array.
num_returns : np.ndarray
The number of returns of the points as (n,) dimensional numpy array.
"""
points: np.ndarray = field(default_factory=lambda: np.empty((0, 3)))
classification: np.ndarray = field(default_factory=lambda: np.empty(0))
intensity: np.ndarray = field(default_factory=lambda: np.empty(0))
return_number: np.ndarray = field(default_factory=lambda: np.empty(0))
num_returns: np.ndarray = field(default_factory=lambda: np.empty(0))
[docs]
def __str__(self):
"""
Return a string representation of the PointCloud, containing its boundaries
and number of points.
Returns
-------
str
A string representation of the PointCloud.
"""
return f"DTCC PointCloud on {self.bounds} with {len(self.points)} points"
[docs]
def __len__(self):
"""
Get the number of points in the PointCloud.
Returns
-------
int
The number of points in the PointCloud.
"""
return self.points.shape[0]
[docs]
def used_classifications(self) -> set:
"""
Get the set of unique classifications used in the PointCloud.
Returns
-------
set
A set containing unique classifications.
"""
return set(np.unique(self.classification))
[docs]
def calculate_bounds(self):
"""
Calculate the bounds of the point cloud and update the bounds attribute.
Returns
-------
None
"""
"""Calculate the bounds of the point cloud and update the bounds attribute."""
if len(self.points) == 0:
self._bounds = Bounds()
else:
self._bounds = Bounds(
xmin=self.points[:, 0].min(),
xmax=self.points[:, 0].max(),
ymin=self.points[:, 1].min(),
ymax=self.points[:, 1].max(),
zmin=self.points[:, 2].min(),
zmax=self.points[:, 2].max(),
)
return self._bounds
[docs]
def remove_points(self, indices: np.ndarray):
"""
Remove points from the point cloud using the given indices.
Parameters
----------
indices : np.ndarray
An array of indices specifying which points to remove.
Returns
-------
None
"""
self.points = np.delete(self.points, indices, axis=0)
if len(self.classification) > 0:
self.classification = np.delete(self.classification, indices, axis=0)
if len(self.intensity) > 0:
self.intensity = np.delete(self.intensity, indices, axis=0)
if len(self.return_number) > 0:
self.return_number = np.delete(self.return_number, indices, axis=0)
if len(self.num_returns) > 0:
self.num_returns = np.delete(self.num_returns, indices, axis=0)
return self
[docs]
def merge(self, other):
"""
Merge another point cloud into this point cloud.
Parameters
----------
other : PointCloud
Another PointCloud object to merge into this one.
Returns
-------
None
"""
if len(other.points) == 0:
return self
if len(self.points) == 0:
self.points = other.points
else:
self.points = np.concatenate((self.points, other.points))
if len(other.classification) == len(other.points):
self.classification = np.concatenate(
(self.classification, other.classification)
)
if len(other.intensity) == len(other.points):
self.intensity = np.concatenate((self.intensity, other.intensity))
if len(other.return_number) == len(other.points):
self.return_number = np.concatenate(
(self.return_number, other.return_number)
)
if len(other.num_returns) == len(other.points):
self.num_returns = np.concatenate((self.num_returns, other.num_returns))
self.calculate_bounds()
return self
[docs]
def to_proto(self) -> proto.Geometry:
"""Return a protobuf representation of the PointCloud.
Returns
-------
proto.Geometry
A protobuf representation of the PointCloud and as a Geometry.
"""
# Handle Geometry fields
pb = Geometry.to_proto(self)
# Handle specific fields
_pb = proto.PointCloud()
_pb.points.extend(self.points.flatten())
_pb.classification.extend(self.classification)
_pb.intensity.extend(self.intensity)
_pb.return_number.extend(self.return_number)
_pb.num_returns.extend(self.num_returns)
pb.point_cloud.CopyFrom(_pb)
return pb
[docs]
def from_proto(self, pb: Union[proto.Geometry, bytes]):
"""Initialize PointCloud from a protobuf representation.
Parameters
----------
pb: Union[proto.Geometry, bytes]
The protobuf message or its serialized bytes representation.
"""
# Handle byte representation
if isinstance(pb, bytes):
pb = proto.Geometry.FromString(pb)
# Handle Geometry fields
Geometry.from_proto(self, pb)
# Handle specific fields
_pb = pb.point_cloud
self.points = np.array(_pb.points).reshape(-1, 3)
self.classification = np.array(_pb.classification).astype(np.uint8)
self.intensity = np.array(_pb.intensity).astype(np.uint16)
self.return_number = np.array(_pb.return_number).astype(np.uint8)
self.num_returns = np.array(_pb.num_returns).astype(np.uint8)