Source code for menpo.shape.labelled

import warnings
from collections import OrderedDict

import numpy as np

from menpo.base import Copyable
from menpo.shape import PointCloud, PointUndirectedGraph, TriMesh
from menpo.shape.graph import PointGraph, _convert_edges_to_symmetric_adjacency_matrix


def indices_to_masks(labels_to_indices, n_points):
    r"""
    Take a dictionary of labels to indices and convert it to a dictionary
    that maps labels to masks. This dictionary is the correct format for
    constructing a :map:`LabelledPointUndirectedGraph`.

    Parameters
    ----------
    labels_to_indices : `ordereddict` {`str` -> `int ndarray`}
        For each label, the indices in to the pointcloud that belong to the
        label.
    n_points : `int`
        Number of points in the pointcloud that is being masked.
    """
    if not isinstance(labels_to_indices, OrderedDict):
        raise ValueError(
            "Must provide an OrderedDict to maintain the "
            "semantic meaning of the labels."
        )

    masks = OrderedDict()
    for label in labels_to_indices:
        indices = labels_to_indices[label]
        mask = np.zeros(n_points, dtype=np.bool)
        mask[indices] = True
        masks[label] = mask
    return masks


[docs]class LabelledPointUndirectedGraph(PointUndirectedGraph): r""" A subclass of :map:`PointUndirectedGraph` that allows the attaching of 'labels' associated with semantic parts of an object. For example, for a face the semantic parts might be the eyes, nose and mouth. These 'labels' are defined as a dictionary of string keys that map to boolean mask arrays that define which of the underlying points belong to a given label. The labels to masks must be within an `OrderedDict` so that semantic ordering can be maintained. Parameters ---------- points : `ndarray` The points representing the landmarks. adjacency_matrix : ``(n_vertices, n_vertices, )`` `ndarray` or `csr_matrix` The adjacency matrix of the graph. The non-edges must be represented with zeros and the edges can have a weight value. :Note: ``adjacency_matrix`` must be symmetric. labels_to_masks : `ordereddict` {`str` -> `bool ndarray`} For each label, the mask that specifies the indices in to the points that belong to the label. copy : `bool`, optional If ``True``, a copy of the data is stored. Raises ------ ValueError If `dict` passed instead of `OrderedDict` ValueError If no set of label masks is passed. ValueError If any of the label masks differs in size to the points. ValueError If there exists any point in the points that is not covered by a label. """ def __init__( self, points, adjacency_matrix, labels_to_masks, copy=True, skip_checks=False ): PointUndirectedGraph.__init__( self, points, adjacency_matrix, copy=copy, skip_checks=skip_checks ) if not labels_to_masks: raise ValueError( "Labelled point graphs are designed to be " "immutable. Empty label sets are not permitted." ) if np.vstack(list(labels_to_masks.values())).shape[1] != points.shape[0]: raise ValueError( "Each mask must have the same number of points " "as the given points." ) if not isinstance(labels_to_masks, OrderedDict): raise ValueError( "Must provide an OrderedDict to maintain the " "semantic meaning of the labels." ) # Another sanity check self._labels_to_masks = labels_to_masks self._verify_all_labels_masked() if copy: self._labels_to_masks = OrderedDict( [(l, m.copy()) for l, m in labels_to_masks.items()] )
[docs] @classmethod def init_with_all_label(cls, points, adjacency_matrix, copy=True): r""" Static constructor to create a :map:`LabelledPointUndirectedGraph` with a single default 'all' label that covers all points. Parameters ---------- points : `ndarray` The points representing the landmarks. adjacency_matrix : ``(n_vertices, n_vertices, )`` `ndarray` or `csr_matrix` The adjacency matrix of the graph. The non-edges must be represented with zeros and the edges can have a weight value. :Note: ``adjacency_matrix`` must be symmetric. copy : `bool`, optional If ``True``, a copy of data is stored on the group. Returns ------- labelled_pointgraph : :map:`LabelledPointUndirectedGraph` Labelled pointgraph wrapping the given points with a single label called 'all' that is ``True`` for all points. """ labels_to_masks = OrderedDict( [("all", np.ones(points.shape[0], dtype=np.bool))] ) return LabelledPointUndirectedGraph( points, adjacency_matrix, labels_to_masks, copy=copy )
[docs] @classmethod def init_from_indices_mapping(cls, points, adjacency, labels_to_indices, copy=True): r""" Static constructor to create a :map:`LabelledPointUndirectedGraph` from an ordered dictionary that maps a set of indices . Parameters ---------- points : :map:`PointCloud` The points representing the landmarks. adjacency : ``(n_vertices, n_vertices, )`` `ndarray`, `csr_matrix` or `list` of edges The adjacency matrix of the graph, or a list of edges representing adjacency. labels_to_indices : `ordereddict` {`str` -> `int ndarray`} For each label, the indices in to the points that belong to the label. copy : `boolean`, optional If ``True``, a copy of the data is stored on the group. Returns ------- labelled_pointgraph : :map:`LabelledPointUndirectedGraph` Labelled point undirected graph wrapping the given points with the given semantic labels applied. Raises ------ ValueError If `dict` passed instead of `OrderedDict` ValueError If any of the label masks differs in size to the points. ValueError If there exists any point in the points that is not covered by a label. """ adjacency = np.array(adjacency) if adjacency.shape[0] != adjacency.shape[1] and adjacency.shape[1] == 2: adjacency = _convert_edges_to_symmetric_adjacency_matrix( adjacency, points.shape[0] ) labels_to_masks = indices_to_masks(labels_to_indices, points.shape[0]) return LabelledPointUndirectedGraph( points, adjacency, labels_to_masks, copy=copy )
[docs] @classmethod def init_from_edges( cls, points, edges, labels_to_masks, copy=True, skip_checks=False ): r""" Construct a :map:`LabelledPointUndirectedGraph` from an edges array. See :map:`PointUndirectedGraph` for more information. Parameters ---------- points : ``(n_vertices, n_dims, )`` `ndarray` The array of point locations. edges : ``(n_edges, 2, )`` `ndarray` or ``None`` The `ndarray` of edges, i.e. all the pairs of vertices that are connected with an edge. If ``None``, then an empty adjacency matrix is created. labels_to_masks : `ordereddict` `{str -> bool ndarray}` For each label, the mask that specifies the indices in to the points that belong to the label. copy : `bool`, optional If ``False``, the `adjacency_matrix` will not be copied on assignment. skip_checks : `bool`, optional If ``True``, no checks will be performed. """ adjacency_matrix = _convert_edges_to_symmetric_adjacency_matrix( edges, points.shape[0] ) return cls( points, adjacency_matrix, labels_to_masks, copy=copy, skip_checks=skip_checks, )
def __setstate__(self, state_dict): # TODO: Deprecate this - this handles importing old-style LandmarkGroup if "_pointcloud" in state_dict: from menpo.base import MenpoDeprecationWarning warnings.warn( "menpo.landmark.LandmarkGroup is now deprecated and " "has been moved to menpo.shape.LandmarkGroup.", MenpoDeprecationWarning, ) _pointcloud = state_dict.pop("_pointcloud") state_dict["points"] = _pointcloud.points # the shape on old landmarks *itself* was allowed to have landmarks # (of course it was very frequently None though, see # https://github.com/menpo/menpo/blob/v0.7.7/menpo/landmark/base.py#L24) # In the new word, self has the same behavior, so move the # landmarks across here. # In the vast majority of cases, this will simply be None. state_dict["_landmarks"] = _pointcloud._landmarks if type(_pointcloud) == PointCloud: adj_mat = _convert_edges_to_symmetric_adjacency_matrix( [], _pointcloud.n_points ) elif isinstance(_pointcloud, PointGraph): a = _pointcloud.adjacency_matrix # Ensure that the matrix is symmetric adj_mat = a.maximum(a.T) elif isinstance(_pointcloud, TriMesh): warnings.warn( "menpo.landmark.LandmarkGroup is now deprecated." "The underlying ._pointcloud was a " "menpo.shape.TriMesh and this has been cast down " "to an UndirectedPointGraph subclass." ) adj_mat = _pointcloud.as_pointgraph(copy=False).adjacency_matrix else: raise ValueError( "Unexpected PointCloud type ({})".format(type(_pointcloud)) ) state_dict["adjacency_matrix"] = adj_mat self.__dict__.update(state_dict)
[docs] def copy(self): r""" Generate an efficient copy of this :map:`LabelledPointUndirectedGraph`. Returns ------- ``type(self)`` A copy of this object """ new = Copyable.copy(self) for k, v in new._labels_to_masks.items(): new._labels_to_masks[k] = v.copy() return new
[docs] def add_label(self, label, indices): """ Add a new label by creating a new mask over the points. A new :map:`LabelledPointUndirectedGraph` is returned. Parameters ---------- label : `string` Label of landmark. indices : ``(K,)`` `ndarray` Array of indices in to the points. Each index implies membership to the label. Returns ------- labelled_pointgraph : :map:`LabelledPointUndirectedGraph` A new labelled pointgraph with the new label specified by indices. """ new = self.copy() mask = np.zeros(self.n_points, dtype=np.bool) mask[indices] = True new._labels_to_masks[label] = mask return new
[docs] def get_label(self, label): """ Returns a new :map:`PointUndirectedGraph` that contains the subset of points that this label represents. Parameters ---------- label : `string` Label to filter on. Returns ------- graph : :map:`PointUndirectedGraph` The PointUndirectedGraph containing the subset of points that this label masks. Will be a subset of the entire group's points. """ mask = self._labels_to_masks[label] return PointUndirectedGraph.from_mask(self, mask)
[docs] def remove_label(self, label): """ Returns a new :map:`LabelledPointUndirectedGraph` that does not contain the given label. .. note:: You cannot delete a semantic label and leave the labelled point graph partially unlabelled. Labelled point graphs must contain labels for **every point**. Parameters --------- label : `string` The label to remove. Raises ------ ValueError If deleting the label would leave some points unlabelled. """ new = self.copy() # Pop the value off, which is akin to deleting it (removes it from the # underlying dict). However, we keep it around so we can check if # removing it causes an unlabelled point new._labels_to_masks.pop(label) new._verify_all_labels_masked() return new
@property def labels(self): """ The list of labels that belong to this group. :type: `list` of `str` """ # Convert to list so that we can index immediately, as keys() # is a view in Python 3 return list(self._labels_to_masks.keys()) @property def n_labels(self): """ Number of labels in the group. :type: `int` """ return len(self.labels) @property def n_landmarks(self): """ The total number of points in the group. :type: `int` """ from menpo.base import MenpoDeprecationWarning warnings.warn( "The .n_landmarks property is deprecated. LandmarkGroups " "are now LabelledPointUndirectedGraph which " "are subclasses of UndirectedPointGraph and thus may " "be used as such. Thus .n_landmarks is an alias for " ".n_points .", MenpoDeprecationWarning, ) return self.n_points
[docs] def with_labels(self, labels): """A new labelled point undirected graph that contains only the given labels. Parameters ---------- labels : `str` or `list` of `str` Label(s) that should be kept in the returned labelled point graph. Returns ------- labelled_pointgraph : :map:`LabelledPointUndirectedGraph` A new labelled point undirected graph with the same group label but containing only the given label(s). """ # Make it easier to use by accepting a single string as well as a list if isinstance(labels, str): labels = [labels] return self._new_group_with_only_labels(labels)
[docs] def without_labels(self, labels): """A new labelled point undirected graph that excludes certain labels. Parameters ---------- labels : `str` or `list` of `str` Label(s) that should be excluded in the returned labelled point graph. Returns ------- labelled_pointgraph : :map:`LabelledPointUndirectedGraph` A new labelled point undirected graph with the same group label but containing all labels except the given label. """ # Make it easier to use by accepting a single string as well as a list if isinstance(labels, str): labels = [labels] labels_to_keep = list(set(self.labels).difference(labels)) return self._new_group_with_only_labels(labels_to_keep)
def _verify_all_labels_masked(self): """ Verify that every point in the pointcloud is associated with a label. If any one point is not covered by a label, then raise a ``ValueError``. """ # values is a generator in Python 3, so convert to list labels_values = list(self._labels_to_masks.values()) unlabelled_points = np.sum(labels_values, axis=0) == 0 if np.any(unlabelled_points): nonzero = np.nonzero(unlabelled_points) raise ValueError( "Every point in the landmark pointcloud must be labelled. " "Points {0} were unlabelled.".format(nonzero) ) def _new_group_with_only_labels(self, labels): """ Deal with changing indices when you add and remove points. In this case we only deal with building a new dataset that keeps masks. Parameters ---------- labels : list of `string` List of strings of the labels to keep Returns ------- labelled_pointgraph : :map:`LabelledPointUndirectedGraph` The new labelled pointgraph with only the requested labels. """ set_difference = set(labels).difference(self.labels) if len(set_difference) > 0: raise ValueError( "Labels {0} do not exist in the landmark " "group. Available labels are: {1}".format( list(set_difference), self.labels ) ) masks_to_keep = [ self._labels_to_masks[l] for l in labels if l in self._labels_to_masks ] overlap = np.sum(masks_to_keep, axis=0) > 0 masks_to_keep = [l[overlap] for l in masks_to_keep] new_graph = self.from_mask(overlap) return LabelledPointUndirectedGraph( new_graph.points, new_graph.adjacency_matrix, OrderedDict(zip(labels, masks_to_keep)), )
[docs] def tojson(self): r""" Convert this `LabelledPointUndirectedGraph` to a dictionary JSON representation. Returns ------- json : ``dict`` Dictionary conforming to the LJSON v2 specification. """ labels = [ {"mask": mask.nonzero()[0].tolist(), "label": label} for label, mask in self._labels_to_masks.items() ] lms_dict = PointUndirectedGraph.tojson(self) lms_dict["labels"] = labels return lms_dict
[docs] def _view_2d( self, with_labels=None, without_labels=None, group="group", 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=5, marker_face_colour=None, marker_edge_colour=None, marker_edge_width=1.0, 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=True, 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.0), 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=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, axes_x_ticks=None, axes_y_ticks=None, figure_size=(10, 8), ): """ Visualize the labelled point undirected graph. Parameters ---------- 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. group : `str` or `None`, optional The name of the labelled point undirected graph. It is used in the legend. 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 x and y axes are flipped. 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 It can either be one of the above or a `list` of those defining a value per label. 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. 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 It can either be one of the above or a `list` of those defining a value per label. marker_edge_colour : See Below, optional The edge colour of the markers. Example options :: {r, g, b, c, m, k, w} or (3, ) ndarray It can either be one of the above or a `list` of those defining a value per label. 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. Possible 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. Possible 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`), 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` or (`float`, `float`) or ``None``, optional The limits of the x axis. If `float`, then it sets padding on the right and left of the LabelledPointUndirectedGraph as a percentage of the LabelledPointUndirectedGraph's width. If `tuple` or `list`, then it defines the axis limits. If ``None``, then the limits are set automatically. axes_y_limits : (`float`, `float`) `tuple` or ``None``, optional The limits of the y axis. If `float`, then it sets padding on the top and bottom of the LabelledPointUndirectedGraph as a percentage of the LabelledPointUndirectedGraph's height. If `tuple` or `list`, then it defines the axis limits. If ``None``, then the limits are set automatically. axes_x_ticks : `list` or `tuple` or ``None``, optional The ticks of the x axis. axes_y_ticks : `list` or `tuple` or ``None``, optional The ticks of the y axis. figure_size : (`float`, `float`) or `None`, optional The size of the figure in inches. Raises ------ ValueError If both ``with_labels`` and ``without_labels`` are passed. """ from menpo.visualize import LandmarkViewer2d if with_labels is not None and without_labels is not None: raise ValueError( "You may only pass one of `with_labels` or " "`without_labels`." ) elif with_labels is not None: lmark_group = self.with_labels(with_labels) elif without_labels is not None: lmark_group = self.without_labels(without_labels) else: lmark_group = self # Fall through landmark_viewer = LandmarkViewer2d(figure_id, new_figure, group, lmark_group) return landmark_viewer.render( 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, axes_x_ticks=axes_x_ticks, axes_y_ticks=axes_y_ticks, figure_size=figure_size, )
def _view_3d( self, with_labels=None, without_labels=None, group="group", figure_id=None, new_figure=False, render_lines=True, line_colour=None, line_width=2, render_markers=True, marker_style="sphere", marker_size=None, marker_colour=None, marker_resolution=8, step=None, alpha=1.0, render_numbering=False, numbers_colour="k", numbers_size=None, ): try: from menpo3d.visualize import LandmarkViewer3d if with_labels is not None and without_labels is not None: raise ValueError( "You may only pass one of `with_labels` or " "`without_labels`." ) elif with_labels is not None: lmark_group = self.with_labels(with_labels) elif without_labels is not None: lmark_group = self.without_labels(without_labels) else: lmark_group = self # Fall through landmark_viewer = LandmarkViewer3d( figure_id, new_figure, group, lmark_group ) return landmark_viewer.render( render_lines=render_lines, line_colour=line_colour, line_width=line_width, render_markers=render_markers, marker_style=marker_style, marker_size=marker_size, marker_colour=marker_colour, marker_resolution=marker_resolution, step=step, alpha=alpha, render_numbering=render_numbering, numbers_colour=numbers_colour, numbers_size=numbers_size, ) except ImportError as e: from menpo.visualize import Menpo3dMissingError raise Menpo3dMissingError(e) def __str__(self): return "{}: n_labels: {}, n_points: {}, n_edges: {}".format( type(self).__name__, self.n_labels, self.n_points, self.n_edges )