Source code for pyntcloud.structures.voxelgrid

import numpy as np

from scipy.spatial import cKDTree

from .base import Structure
from ..plot.voxelgrid import plot_voxelgrid
from ..utils.array import cartesian

try:
    from ..utils.numba import groupby_max, groupby_count, groupby_sum
    is_numba_avaliable = True
except ImportError:
    is_numba_avaliable = False


[docs]class VoxelGrid(Structure): def __init__(self, *, points, colors=None, n_x=1, n_y=1, n_z=1, size_x=None, size_y=None, size_z=None, regular_bounding_box=True): """Grid of voxels with support for different build methods. Parameters ---------- points: (N, 3) numpy.array colors: (N, 3) numpy.array, optional Default None. If not None, color for each voxel will be computed. n_x, n_y, n_z : int, optional Default: 1 The number of segments in which each axis will be divided. Ignored if corresponding size_x, size_y or size_z is not None. size_x, size_y, size_z : float, optional Default: None The desired voxel size along each axis. If not None, the corresponding n_x, n_y or n_z will be ignored. regular_bounding_box : bool, optional Default: True If True, the bounding box of the point cloud will be adjusted in order to have all the dimensions of equal length. """ super().__init__(points=points) self.colors = colors self.x_y_z = np.asarray([n_x, n_y, n_z]) self.sizes = np.asarray([size_x, size_y, size_z]) self.regular_bounding_box = regular_bounding_box self.id = None self.xyzmin, self.xyzmax = None, None self.segments = None self.shape = None self.n_voxels = None self.voxel_x, self.voxel_y, self.voxel_z = None, None, None self.voxel_n = None self.voxel_centers = None self.voxel_colors = None @classmethod def extract_info(cls, pyntcloud): """ABC API""" try: colors = pyntcloud.points[['red', 'green', 'blue']].to_numpy() except KeyError: colors = None info = { "points": pyntcloud.xyz, "colors": colors } return info def compute(self): """ABC API.""" xyzmin = self._points.min(0) xyzmax = self._points.max(0) xyz_range = self._points.ptp(0) if self.regular_bounding_box: #: adjust to obtain a minimum bounding box with all sides of equal length margin = max(xyz_range) - xyz_range xyzmin = xyzmin - margin / 2 xyzmax = xyzmax + margin / 2 for n, size in enumerate(self.sizes): if size is None: continue margin = (((self._points.ptp(0)[n] // size) + 1) * size) - self._points.ptp(0)[n] xyzmin[n] -= margin / 2 xyzmax[n] += margin / 2 self.x_y_z[n] = ((xyzmax[n] - xyzmin[n]) / size).astype(int) self.xyzmin = xyzmin self.xyzmax = xyzmax segments = [] shape = [] for i in range(3): # note the +1 in num s, step = np.linspace(xyzmin[i], xyzmax[i], num=(self.x_y_z[i] + 1), retstep=True) segments.append(s) shape.append(step) self.segments = segments self.shape = shape self.n_voxels = np.prod(self.x_y_z) self.id = "V({},{},{})".format(self.x_y_z, self.sizes, self.regular_bounding_box) # find where each point lies in corresponding segmented axis # -1 so index are 0-based; clip for edge cases self.voxel_x = np.clip(np.searchsorted(self.segments[0], self._points[:, 0]) - 1, 0, self.x_y_z[0]) self.voxel_y = np.clip(np.searchsorted(self.segments[1], self._points[:, 1]) - 1, 0, self.x_y_z[1]) self.voxel_z = np.clip(np.searchsorted(self.segments[2], self._points[:, 2]) - 1, 0, self.x_y_z[2]) self.voxel_n = np.ravel_multi_index([self.voxel_x, self.voxel_y, self.voxel_z], self.x_y_z) # compute center of each voxel midsegments = [(self.segments[i][1:] + self.segments[i][:-1]) / 2 for i in range(3)] self.voxel_centers = cartesian(midsegments).astype(np.float32) # compute voxel colors if self.colors is not None: order = np.argsort(self.voxel_n) _, breaks, counts = np.unique(self.voxel_n[order], return_index=True, return_counts=True) repeated_counts = np.repeat(counts[:, None], 3, axis=1) # why square? watch this: https://www.youtube.com/watch?v=LKnqECcg6Gw squared_colors = np.square(self.colors[order].astype(np.int64)) summed_colors = np.add.reduceat(squared_colors, breaks, axis=0) averaged_colors = np.sqrt(summed_colors / repeated_counts) self.voxel_colors = np.rint(averaged_colors).astype(np.uint8) def query(self, points): """ABC API. Query structure. TODO Make query_voxelgrid an independent function, and add a light save mode where only segments and x_y_z are saved. """ voxel_x = np.clip(np.searchsorted( self.segments[0], points[:, 0]) - 1, 0, self.x_y_z[0]) voxel_y = np.clip(np.searchsorted( self.segments[1], points[:, 1]) - 1, 0, self.x_y_z[1]) voxel_z = np.clip(np.searchsorted( self.segments[2], points[:, 2]) - 1, 0, self.x_y_z[2]) voxel_n = np.ravel_multi_index([voxel_x, voxel_y, voxel_z], self.x_y_z) return voxel_n def get_feature_vector(self, mode="binary"): """Return a vector of size self.n_voxels. See mode options below. Parameters ---------- mode: str in available modes. See Notes Default "binary" Returns ------- feature_vector: [n_x, n_y, n_z] ndarray See Notes. Notes ----- Available modes are: binary 0 for empty voxels, 1 for occupied. density number of points inside voxel / total number of points. TDF Truncated Distance Function. Value between 0 and 1 indicating the distance between the voxel's center and the closest point. 1 on the surface, 0 on voxels further than 2 * voxel side. x_max, y_max, z_max Maximum coordinate value of points inside each voxel. x_mean, y_mean, z_mean Mean coordinate value of points inside each voxel. """ vector = np.zeros(self.n_voxels) if mode == "binary": vector[np.unique(self.voxel_n)] = 1 elif mode == "density": count = np.bincount(self.voxel_n) vector[:len(count)] = count vector /= len(self.voxel_n) elif mode == "TDF": # truncation = np.linalg.norm(self.shape) kdt = cKDTree(self._points) vector, i = kdt.query(self.voxel_centers, n_jobs=-1) elif mode.endswith("_max"): if not is_numba_avaliable: raise ImportError("numba is required to compute {}".format(mode)) axis = {"x_max": 0, "y_max": 1, "z_max": 2} vector = groupby_max(self._points, self.voxel_n, axis[mode], vector) elif mode.endswith("_mean"): if not is_numba_avaliable: raise ImportError("numba is required to compute {}".format(mode)) axis = {"x_mean": 0, "y_mean": 1, "z_mean": 2} voxel_sum = groupby_sum(self._points, self.voxel_n, axis[mode], np.zeros(self.n_voxels)) voxel_count = groupby_count(self._points, self.voxel_n, np.zeros(self.n_voxels)) vector = np.nan_to_num(voxel_sum / voxel_count) else: raise NotImplementedError("{} is not a supported feature vector mode".format(mode)) return vector.reshape(self.x_y_z) def get_voxel_neighbors(self, voxel): """Get valid, non-empty 26 neighbors of voxel. Parameters ---------- voxel: int in self.set_voxel_n Returns ------- neighbors: list of int Indices of the valid, non-empty 26 neighborhood around voxel. """ x, y, z = np.unravel_index(voxel, self.x_y_z) valid_x = [] valid_y = [] valid_z = [] if x - 1 >= 0: valid_x.append(x - 1) if y - 1 >= 0: valid_y.append(y - 1) if z - 1 >= 0: valid_z.append(z - 1) valid_x.append(x) valid_y.append(y) valid_z.append(z) if x + 1 < self.x_y_z[0]: valid_x.append(x + 1) if y + 1 < self.x_y_z[1]: valid_y.append(y + 1) if z + 1 < self.x_y_z[2]: valid_z.append(z + 1) valid_neighbor_indices = cartesian((valid_x, valid_y, valid_z)) ravel_indices = np.ravel_multi_index((valid_neighbor_indices[:, 0], valid_neighbor_indices[:, 1], valid_neighbor_indices[:, 2]), self.x_y_z) return [x for x in ravel_indices if x in np.unique(self.voxel_n)] def plot(self, d=3, mode="binary", backend='pythreejs', cmap="Oranges", axis=False, output_name=None, width=800, height=500): return plot_voxelgrid(self, d=d, mode=mode, backend=backend, cmap=cmap, axis=axis, output_name=output_name, width=width, height=height)