import numpy as np
from warnings import warn
from scipy.spatial.distance import cdist
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 : `bool`, 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:
if not points.flags.c_contiguous:
warn('The copy flag was NOT honoured. A copy HAS been made. '
'Please ensure the data you pass is C-contiguous.')
points = np.array(points, copy=True, order='C')
else:
points = np.array(points, copy=True, order='C')
self.points = points
@property
def n_points(self):
r"""
The number of points in the pointcloud.
:type: `int`
"""
return self.points.shape[0]
@property
def n_dims(self):
r"""
The number of dimensions in the pointcloud.
:type: `int`
"""
return self.points.shape[1]
[docs] def h_points(self):
r"""
Convert poincloud to a homogeneous array: ``(n_dims + 1, n_points)``
:type: ``type(self)``
"""
return np.concatenate((self.points.T, np.ones(self.n_points)[None, :]))
[docs] def centre(self):
r"""
The mean of all the points in this PointCloud (centre of mass).
Returns
-------
centre : ``(n_dims)`` `ndarray`
The mean of this PointCloud's points.
"""
return np.mean(self.points, axis=0)
[docs] def centre_of_bounds(self):
r"""
The centre of the absolute bounds of this PointCloud. Contrast with
:meth:`centre`, which is the mean point position.
Returns
-------
centre : ``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 :map:`PointCloud` to a dictionary representation suitable
for inclusion in the LJSON landmark format.
Returns
-------
json : `dict`
Dictionary with ``points`` keys.
"""
return {'points': self.points.tolist()}
[docs] def from_vector_inplace(self, vector):
r"""
Updates the points of this PointCloud in-place with the reshaped points
from the provided vector. Note that the vector should have the form
``[x0, y0, x1, y1, ....., xn, yn]`` for 2D.
Parameters
----------
vector : ``(n_points,)`` `ndarray`
The vector from which to create the points' array.
"""
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 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 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
[docs] def bounding_box(self):
r"""
Return the bounding box of this PointCloud as a directed graph.
The the first point (0) will be nearest the origin for an axis aligned
Pointcloud.
In the case of an image, this ordering would appear as:
::
0<--3
| ^
| |
v |
1-->2
Returns
-------
bounding_box : :map:`PointDirectedGraph`
The axis aligned bounding box of the PointCloud.
"""
from .graph import PointDirectedGraph
min_p, max_p = self.bounds()
return PointDirectedGraph(np.array([min_p, [max_p[0], min_p[1]],
max_p, [min_p[0], max_p[1]]]),
np.array([[0, 1], [1, 2], [2, 3], [3, 0]]),
copy=False)
[docs] def _view_2d(self, figure_id=None, new_figure=False, image_view=True,
render_markers=True, marker_style='o', marker_size=20,
marker_face_colour='r', marker_edge_colour='k',
marker_edge_width=1., render_axes=True,
axes_font_name='sans-serif', axes_font_size=10,
axes_font_style='normal', axes_font_weight='normal',
axes_x_limits=None, axes_y_limits=None, figure_size=(10, 8),
label=None, **kwargs):
r"""
Visualization of the PointCloud in 2D.
Returns
-------
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
image_view : `bool`, optional
If ``True`` the PointCloud will be viewed as if it is in the image
coordinate system.
render_lines : `bool`, optional
If ``True``, the edges will be rendered.
line_colour : See Below, optional
The colour of the lines.
Example options::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
line_style : ``{-, --, -., :}``, optional
The style of the lines.
line_width : `float`, optional
The width of the lines.
render_markers : `bool`, optional
If ``True``, the markers will be rendered.
marker_style : See Below, optional
The style of the markers. Example options ::
{., ,, o, v, ^, <, >, +, x, D, d, s, p, *, h, H, 1, 2, 3, 4, 8}
marker_size : `int`, optional
The size of the markers in points^2.
marker_face_colour : See Below, optional
The face (filling) colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_colour : See Below, optional
The edge colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_width : `float`, optional
The width of the markers' edge.
render_axes : `bool`, optional
If ``True``, the axes will be rendered.
axes_font_name : See Below, optional
The font of the axes.
Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
axes_font_size : `int`, optional
The font size of the axes.
axes_font_style : {``normal``, ``italic``, ``oblique``}, optional
The font style of the axes.
axes_font_weight : See Below, optional
The font weight of the axes.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold, demibold, demi, bold, heavy, extra bold, black}
axes_x_limits : (`float`, `float`) `tuple` or ``None``, optional
The limits of the x axis.
axes_y_limits : (`float`, `float`) `tuple` or ``None``, optional
The limits of the y axis.
figure_size : (`float`, `float`) `tuple` or ``None``, optional
The size of the figure in inches.
label : `str`, optional
The name entry in case of a legend.
Returns
-------
viewer : :map:`PointGraphViewer2d`
The viewer object.
"""
from menpo.visualize.base import PointGraphViewer2d
adjacency_array = np.empty(0)
renderer = PointGraphViewer2d(figure_id, new_figure,
self.points,
adjacency_array)
renderer.render(
image_view=image_view, render_lines=False, line_colour='b',
line_style='-', line_width=1., render_markers=render_markers,
marker_style=marker_style, marker_size=marker_size,
marker_face_colour=marker_face_colour,
marker_edge_colour=marker_edge_colour,
marker_edge_width=marker_edge_width, render_axes=render_axes,
axes_font_name=axes_font_name, axes_font_size=axes_font_size,
axes_font_style=axes_font_style, axes_font_weight=axes_font_weight,
axes_x_limits=axes_x_limits, axes_y_limits=axes_y_limits,
figure_size=figure_size, label=label)
return renderer
[docs] def _view_landmarks_2d(self, group=None, with_labels=None,
without_labels=None, figure_id=None,
new_figure=False, image_view=True, render_lines=True,
line_colour=None, line_style='-', line_width=1,
render_markers=True, marker_style='o',
marker_size=20, marker_face_colour=None,
marker_edge_colour=None, marker_edge_width=1.,
render_numbering=False,
numbers_horizontal_align='center',
numbers_vertical_align='bottom',
numbers_font_name='sans-serif', numbers_font_size=10,
numbers_font_style='normal',
numbers_font_weight='normal',
numbers_font_colour='k', render_legend=False,
legend_title='', legend_font_name='sans-serif',
legend_font_style='normal', legend_font_size=10,
legend_font_weight='normal',
legend_marker_scale=None, legend_location=2,
legend_bbox_to_anchor=(1.05, 1.),
legend_border_axes_pad=None, legend_n_columns=1,
legend_horizontal_spacing=None,
legend_vertical_spacing=None, legend_border=True,
legend_border_padding=None, legend_shadow=False,
legend_rounded_corners=False, render_axes=False,
axes_font_name='sans-serif', axes_font_size=10,
axes_font_style='normal', axes_font_weight='normal',
axes_x_limits=None, axes_y_limits=None,
figure_size=(10, 8)):
"""
Visualize the landmarks. This method will appear on the Image as
``view_landmarks`` if the Image is 2D.
Parameters
----------
group : `str` or``None`` optional
The landmark group to be visualized. If ``None`` and there are more
than one landmark groups, an error is raised.
with_labels : ``None`` or `str` or `list` of `str`, optional
If not ``None``, only show the given label(s). Should **not** be
used with the ``without_labels`` kwarg.
without_labels : ``None`` or `str` or `list` of `str`, optional
If not ``None``, show all except the given label(s). Should **not**
be used with the ``with_labels`` kwarg.
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
image_view : `bool`, optional
If ``True`` the PointCloud will be viewed as if it is in the image
coordinate system.
render_lines : `bool`, optional
If ``True``, the edges will be rendered.
line_colour : See Below, optional
The colour of the lines.
Example options::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
line_style : ``{-, --, -., :}``, optional
The style of the lines.
line_width : `float`, optional
The width of the lines.
render_markers : `bool`, optional
If ``True``, the markers will be rendered.
marker_style : See Below, optional
The style of the markers. Example options ::
{., ,, o, v, ^, <, >, +, x, D, d, s, p, *, h, H, 1, 2, 3, 4, 8}
marker_size : `int`, optional
The size of the markers in points^2.
marker_face_colour : See Below, optional
The face (filling) colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_colour : See Below, optional
The edge colour of the markers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
marker_edge_width : `float`, optional
The width of the markers' edge.
render_numbering : `bool`, optional
If ``True``, the landmarks will be numbered.
numbers_horizontal_align : ``{center, right, left}``, optional
The horizontal alignment of the numbers' texts.
numbers_vertical_align : ``{center, top, bottom, baseline}``, optional
The vertical alignment of the numbers' texts.
numbers_font_name : See Below, optional
The font of the numbers. Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
numbers_font_size : `int`, optional
The font size of the numbers.
numbers_font_style : ``{normal, italic, oblique}``, optional
The font style of the numbers.
numbers_font_weight : See Below, optional
The font weight of the numbers.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold, demibold, demi, bold, heavy, extra bold, black}
numbers_font_colour : See Below, optional
The font colour of the numbers.
Example options ::
{r, g, b, c, m, k, w}
or
(3, ) ndarray
render_legend : `bool`, optional
If ``True``, the legend will be rendered.
legend_title : `str`, optional
The title of the legend.
legend_font_name : See below, optional
The font of the legend. Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
legend_font_style : ``{normal, italic, oblique}``, optional
The font style of the legend.
legend_font_size : `int`, optional
The font size of the legend.
legend_font_weight : See Below, optional
The font weight of the legend.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold, demibold, demi, bold, heavy, extra bold, black}
legend_marker_scale : `float`, optional
The relative size of the legend markers with respect to the original
legend_location : `int`, optional
The location of the legend. The predefined values are:
=============== ==
'best' 0
'upper right' 1
'upper left' 2
'lower left' 3
'lower right' 4
'right' 5
'center left' 6
'center right' 7
'lower center' 8
'upper center' 9
'center' 10
=============== ==
legend_bbox_to_anchor : (`float`, `float`) `tuple`, optional
The bbox that the legend will be anchored.
legend_border_axes_pad : `float`, optional
The pad between the axes and legend border.
legend_n_columns : `int`, optional
The number of the legend's columns.
legend_horizontal_spacing : `float`, optional
The spacing between the columns.
legend_vertical_spacing : `float`, optional
The vertical space between the legend entries.
legend_border : `bool`, optional
If ``True``, a frame will be drawn around the legend.
legend_border_padding : `float`, optional
The fractional whitespace inside the legend border.
legend_shadow : `bool`, optional
If ``True``, a shadow will be drawn behind legend.
legend_rounded_corners : `bool`, optional
If ``True``, the frame's corners will be rounded (fancybox).
render_axes : `bool`, optional
If ``True``, the axes will be rendered.
axes_font_name : See Below, optional
The font of the axes. Example options ::
{serif, sans-serif, cursive, fantasy, monospace}
axes_font_size : `int`, optional
The font size of the axes.
axes_font_style : ``{normal, italic, oblique}``, optional
The font style of the axes.
axes_font_weight : See Below, optional
The font weight of the axes.
Example options ::
{ultralight, light, normal, regular, book, medium, roman,
semibold,demibold, demi, bold, heavy, extra bold, black}
axes_x_limits : (`float`, `float`) `tuple` or ``None`` optional
The limits of the x axis.
axes_y_limits : (`float`, `float`) `tuple` or ``None`` optional
The limits of the y axis.
figure_size : (`float`, `float`) `tuple` or ``None`` optional
The size of the figure in inches.
Raises
------
ValueError
If both ``with_labels`` and ``without_labels`` are passed.
ValueError
If the landmark manager doesn't contain the provided group label.
"""
if not self.has_landmarks:
raise ValueError('PointCloud does not have landmarks attached, '
'unable to view landmarks.')
self_view = self.view(figure_id=figure_id, new_figure=new_figure,
image_view=image_view, figure_size=figure_size)
landmark_view = self.landmarks[group].view(
with_labels=with_labels, without_labels=without_labels,
figure_id=self_view.figure_id, new_figure=False,
image_view=image_view, render_lines=render_lines,
line_colour=line_colour, line_style=line_style,
line_width=line_width, render_markers=render_markers,
marker_style=marker_style, marker_size=marker_size,
marker_face_colour=marker_face_colour,
marker_edge_colour=marker_edge_colour,
marker_edge_width=marker_edge_width,
render_numbering=render_numbering,
numbers_horizontal_align=numbers_horizontal_align,
numbers_vertical_align=numbers_vertical_align,
numbers_font_name=numbers_font_name,
numbers_font_size=numbers_font_size,
numbers_font_style=numbers_font_style,
numbers_font_weight=numbers_font_weight,
numbers_font_colour=numbers_font_colour,
render_legend=render_legend, legend_title=legend_title,
legend_font_name=legend_font_name,
legend_font_style=legend_font_style,
legend_font_size=legend_font_size,
legend_font_weight=legend_font_weight,
legend_marker_scale=legend_marker_scale,
legend_location=legend_location,
legend_bbox_to_anchor=legend_bbox_to_anchor,
legend_border_axes_pad=legend_border_axes_pad,
legend_n_columns=legend_n_columns,
legend_horizontal_spacing=legend_horizontal_spacing,
legend_vertical_spacing=legend_vertical_spacing,
legend_border=legend_border,
legend_border_padding=legend_border_padding,
legend_shadow=legend_shadow,
legend_rounded_corners=legend_rounded_corners,
render_axes=render_axes, axes_font_name=axes_font_name,
axes_font_size=axes_font_size, axes_font_style=axes_font_style,
axes_font_weight=axes_font_weight, axes_x_limits=axes_x_limits,
axes_y_limits=axes_y_limits, figure_size=figure_size)
return landmark_view
def _view_3d(self, figure_id=None, new_figure=False):
r"""
Visualization of the PointCloud in 3D.
Parameters
----------
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
Returns
-------
viewer : PointCloudViewer3d
The Menpo3D viewer object.
"""
try:
from menpo3d.visualize import PointCloudViewer3d
return PointCloudViewer3d(figure_id, new_figure,
self.points).render()
except ImportError:
from menpo.visualize import Menpo3dErrorMessage
raise ImportError(Menpo3dErrorMessage)
def _view_landmarks_3d(self, figure_id=None, new_figure=False,
group=None):
r"""
Visualization of the PointCloud landmarks in 3D.
Parameters
----------
figure_id : `object`, optional
The id of the figure to be used.
new_figure : `bool`, optional
If ``True``, a new figure is created.
group : `str`
The landmark group to visualize. If ``None`` is passed, the first
and only landmark group on the object will be visualized.
Returns
-------
viewer : LandmarkViewer3d
The Menpo3D viewer object.
"""
try:
from menpo3d.visualize import LandmarkViewer3d
self_renderer = self.view(figure_id=figure_id,
new_figure=new_figure)
return LandmarkViewer3d(self_renderer.figure, False, self,
self.landmarks[group]).render()
except ImportError:
from menpo.visualize import Menpo3dErrorMessage
raise ImportError(Menpo3dErrorMessage)
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 PointCloud and another.
By default the Euclidean 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 PointCloud. 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.
Raises
------
ValueError
Mask must have same number of points as pointcloud.
"""
if mask.shape[0] != self.n_points:
raise ValueError('Mask must be a 1D boolean array of the same '
'number of entries as points in this PointCloud.')
pc = self.copy()
pc.points = pc.points[mask, :]
return pc