Source code for menpo.shape.pointcloud

import numpy as np
from scipy.spatial.distance import cdist
from menpo.visualize import PointCloudViewer
from menpo.shape.base import Shape


[docs]class PointCloud(Shape): r""" An N-dimensional point cloud. This is internally represented as an ndarray of shape (``n_points``, ``n_dims``). This class is important for dealing with complex functionality such as viewing and representing metadata such as landmarks. Currently only 2D and 3D pointclouds are viewable. Parameters ---------- points : ``(n_points, n_dims)`` `ndarray` The array representing the points. copy : `boolean`, optional If ``False``, the points will not be copied on assignment. Note that this will miss out on additional checks. Further note that we still demand that the array is C-contiguous - if it isn't, a copy will be generated anyway. In general this should only be used if you know what you are doing. """ def __init__(self, points, copy=True): super(PointCloud, self).__init__() if not copy: # Let's check we don't do a copy! points_handle = points self.points = np.require(points, requirements=['C']) if self.points is not points_handle: raise Warning('The copy flag was NOT honoured. ' 'A copy HAS been made. Please ensure the data ' 'you pass is C-contiguous.') else: self.points = np.array(points, copy=True, order='C')
[docs] def copy(self): r""" An efficient copy of this PointCloud. Only landmarks and points will be transferred. For a full copy consider using ``deepcopy()``. Returns ------- pointcloud : :map:`PointCloud` A PointCloud with the same points and landmarks as this one. """ new_pc = PointCloud(self.points, copy=True) new_pc.landmarks = self.landmarks return new_pc
@property
[docs] def h_points(self): r""" homogeneous points of shape (``n_dims + 1``, ``n_points``) """ return np.concatenate((self.points.T, np.ones(self.n_points)[None, :]))
@property
[docs] def n_points(self): r""" The number of points in the pointcloud. :type: `int` """ return self.points.shape[0]
@property
[docs] def n_dims(self): r""" The number of dimensions in the pointcloud. :type: `int` """ return self.points.shape[1]
@property
[docs] def centre(self): r""" The mean of all the points in this PointCloud (in the centre of mass sense) :type: ``(n_dims)`` `ndarray` The mean of this PointCloud's points. """ return np.mean(self.points, axis=0)
@property
[docs] def centre_of_bounds(self): r""" The centre of the absolute bounds of this PointCloud. Contrast with centre, which is the mean point position. :type: ``n_dims`` `ndarray` The centre of the bounds of this PointCloud. """ min_b, max_b = self.bounds() return (min_b + max_b) / 2
def _as_vector(self): r""" Returns a flattened representation of the pointcloud. Note that the flattened representation is of the form ``[x0, y0, x1, y1, ....., xn, yn]`` for 2D. Returns ------- flattened : ``(n_points,)`` `ndarray` The flattened points. """ return self.points.ravel()
[docs] def tojson(self): r""" Convert this PointCloud to a dictionary JSON representation. Returns ------- json_dict : `dict` Dictionary with a 'points' key, the value of which is a list suitable for use in the by the `json` standard library package. """ return {'points': self.points.tolist()}
[docs] def from_vector_inplace(self, vector): r""" Updates this PointCloud in-place with a new vector of parameters """ self.points = vector.reshape([-1, self.n_dims])
def __str__(self): return '{}: n_points: {}, n_dims: {}'.format(type(self).__name__, self.n_points, self.n_dims)
[docs] def bounds(self, boundary=0): r""" The minimum to maximum extent of the :map:`PointCloud`. An optional boundary argument can be provided to expand the bounds by a constant margin. Parameters ---------- boundary : `float` A optional padding distance that is added to the bounds. Default is ``0``, meaning the max/min of tightest possible containing square/cube/hypercube is returned. Returns -------- min_b : ``(n_dims,)`` `ndarray` The minimum extent of the :map:`PointCloud` and boundary along each dimension max_b : ``(n_dims,)`` `ndarray` The maximum extent of the :map:`PointCloud` and boundary along each dimension """ min_b = np.min(self.points, axis=0) - boundary max_b = np.max(self.points, axis=0) + boundary return min_b, max_b
[docs] def range(self, boundary=0): r""" The range of the extent of the :map:`PointCloud`. Parameters ---------- boundary : `float` A optional padding distance that is used to extend the bounds from which the range is computed. Default is ``0``, no extension is performed. Returns ------- range : ``(n_dims,)`` `ndarray` The range of the :map:`PointCloud` extent in each dimension. """ min_b, max_b = self.bounds(boundary) return max_b - min_b
def _view(self, figure_id=None, new_figure=False, **kwargs): return PointCloudViewer(figure_id, new_figure, self.points).render(**kwargs) def _transform_self_inplace(self, transform): self.points = transform(self.points) return self
[docs] def distance_to(self, pointcloud, **kwargs): r""" Returns a distance matrix between this point cloud and another. By default the Euclidian distance is calculated - see `scipy.spatial.distance.cdist` for valid kwargs to change the metric and other properties. Parameters ---------- pointcloud : :map:`PointCloud` The second pointcloud to compute distances between. This must be of the same dimension as this PointCloud. Returns ------- distance_matrix: ``(n_points, n_points)`` `ndarray` The symmetric pairwise distance matrix between the two PointClouds s.t. distance_matrix[i, j] is the distance between the i'th point of this PointCloud and the j'th point of the input PointCloud. """ if self.n_dims != pointcloud.n_dims: raise ValueError("The two PointClouds must be of the same " "dimensionality.") return cdist(self.points, pointcloud.points, **kwargs)
[docs] def norm(self, **kwargs): r""" Returns the norm of this point cloud. This is a translation and rotation invariant measure of the point cloud's intrinsic size - in other words, it is always taken around the point cloud's centre. By default, the Frobenius norm is taken, but this can be changed by setting kwargs - see numpy.linalg.norm for valid options. Returns ------- norm : `float` The norm of this :map:`PointCloud` """ return np.linalg.norm(self.points - self.centre, **kwargs)
[docs] def from_mask(self, mask): """ A 1D boolean array with the same number of elements as the number of points in the pointcloud. This is then broadcast across the dimensions of the pointcloud and returns a new pointcloud containing only those points that were ``True`` in the mask. Parameters ---------- mask : ``(n_points,)`` `ndarray` 1D array of booleans Returns ------- pointcloud : :map:`PointCloud` A new pointcloud that has been masked. """ return PointCloud(self.points[mask, :])
[docs] def update_from_mask(self, mask): """ A 1D boolean array with the same number of elements as the number of points in the pointcloud. This is then broadcast across the dimensions of the pointcloud. The same pointcloud is updated in place. Parameters ---------- mask : ``(n_points,)`` `ndarray` 1D array of booleans Returns ------- pointcloud : :map:`PointCloud` A pointer to self. """ self.points = self.points[mask, :] return self