Source code for menpo.transform.base

import warnings
import numpy as np

from menpo.base import Copyable, MenpoDeprecationWarning


[docs]class Transform(Copyable): r""" Abstract representation of any spatial transform. Provides a unified interface to apply the transform with :meth:`apply_inplace` and :meth:`apply`. All Transforms support basic composition to form a :map:`TransformChain`. There are two useful forms of composition. Firstly, the mathematical composition symbol `o` has the following definition:: Let a(x) and b(x) be two transforms on x. (a o b)(x) == a(b(x)) This functionality is provided by the :meth:`compose_after` family of methods: :: (a.compose_after(b)).apply(x) == a.apply(b.apply(x)) Equally useful is an inversion the order of composition - so that over time a large chain of transforms can be built to do a useful job, and composing on this chain adds another transform to the end (after all other preceding transforms have been performed). For instance, let's say we want to rescale a :map:`PointCloud` ``p`` around its mean, and then translate it some place else. It would be nice to be able to do something like:: t = Translation(-p.centre) # translate to centre s = Scale(2.0) # rescale move = Translate([10, 0 ,0]) # budge along the x axis t.compose(s).compose(-t).compose(move) In Menpo, this functionality is provided by the :meth:`compose_before()` family of methods:: (a.compose_before(b)).apply(x) == b.apply(a.apply(x)) For native composition, see the :map:`ComposableTransform` subclass and the :map:`VComposable` mix-in. For inversion, see the :map:`Invertible` and :map:`VInvertible` mix-ins. For alignment, see the :map:`Alignment` mix-in. """ @property def n_dims(self): r""" The dimensionality of the data the transform operates on. ``None`` if the transform is not dimension specific. :type: `int` or ``None`` """ return None @property def n_dims_output(self): r""" The output of the data from the transform. ``None`` if the output of the transform is not dimension specific. :type: `int` or ``None`` """ # most Transforms don't change the dimensionality of their input. return self.n_dims def _apply(self, x, **kwargs): r""" Applies the transform to the array ``x``, returning the result. This method does the actual work of transforming the data, and is the one that subclasses must implement. :meth:`apply` and :meth:`apply_inplace` both call this method to do that actual work. Parameters ---------- x : ``(n_points, n_dims)`` `ndarray` The array to be transformed. kwargs : `dict` Subclasses may need these in their ``_apply`` methods. Returns ------- transformed : ``(n_points, n_dims_output)`` `ndarray` The transformed array """ raise NotImplementedError()
[docs] def apply_inplace(self, *args, **kwargs): r""" Deprecated as public supported API, use the non-mutating `apply()` instead. For internal performance-specific uses, see `_apply_inplace()`. """ warnings.warn('the public API for inplace operations is deprecated ' 'and will be removed in a future version of Menpo. ' 'Use .apply() instead.', MenpoDeprecationWarning) return self._apply_inplace(*args, **kwargs)
def _apply_inplace(self, x, **kwargs): r""" Applies this transform to a :map:`Transformable` ``x`` destructively. Any ``kwargs`` will be passed to the specific transform :meth:`_apply` method. Note that this is an inplace operation that should be used sparingly, by internal API's where creating a copy of the transformed object is expensive. It does not return anything, as the operation is inplace. Parameters ---------- x : :map:`Transformable` The :map:`Transformable` object to be transformed. kwargs : `dict` Passed through to :meth:`_apply`. """ def transform(x_): """ Local closure which calls the :meth:`_apply` method with the `kwargs` attached. """ return self._apply(x_, **kwargs) try: x._transform_inplace(transform) except AttributeError: raise ValueError('apply_inplace can only be used on Transformable' ' objects.')
[docs] def apply(self, x, batch_size=None, **kwargs): r""" Applies this transform to ``x``. If ``x`` is :map:`Transformable`, ``x`` will be handed this transform object to transform itself non-destructively (a transformed copy of the object will be returned). If not, ``x`` is assumed to be an `ndarray`. The transformation will be non-destructive, returning the transformed version. Any ``kwargs`` will be passed to the specific transform :meth:`_apply` method. Parameters ---------- x : :map:`Transformable` or ``(n_points, n_dims)`` `ndarray` The array or object to be transformed. batch_size : `int`, optional If not ``None``, this determines how many items from the numpy array will be passed through the transform at a time. This is useful for operations that require large intermediate matrices to be computed. kwargs : `dict` Passed through to :meth:`_apply`. Returns ------- transformed : ``type(x)`` The transformed object or array """ def transform(x_): """ Local closure which calls the :meth:`_apply` method with the `kwargs` attached. """ return self._apply_batched(x_, batch_size, **kwargs) try: return x._transform(transform) except AttributeError: return self._apply_batched(x, batch_size, **kwargs)
def _apply_batched(self, x, batch_size, **kwargs): if batch_size is None: return self._apply(x, **kwargs) else: outputs = [] n_points = x.shape[0] for lo_ind in range(0, n_points, batch_size): hi_ind = lo_ind + batch_size outputs.append(self._apply(x[lo_ind:hi_ind], **kwargs)) return np.vstack(outputs)
[docs] def compose_before(self, transform): r""" Returns a :map:`TransformChain` that represents **this** transform composed **before** the given transform:: c = a.compose_before(b) c.apply(p) == b.apply(a.apply(p)) ``a`` and ``b`` are left unchanged. Parameters ---------- transform : :map:`Transform` Transform to be applied **after** self Returns ------- transform : :map:`TransformChain` The resulting transform chain. """ return TransformChain([self, transform])
[docs] def compose_after(self, transform): r""" Returns a :map:`TransformChain` that represents **this** transform composed **after** the given transform:: c = a.compose_after(b) c.apply(p) == a.apply(b.apply(p)) ``a`` and ``b`` are left unchanged. This corresponds to the usual mathematical formalism for the compose operator, `o`. Parameters ---------- transform : :map:`Transform` Transform to be applied **before** self Returns ------- transform : :map:`TransformChain` The resulting transform chain. """ return TransformChain([transform, self])
[docs]class Transformable(Copyable): r""" Interface for objects that know how to be transformed by the :map:`Transform` interface. When ``Transform.apply_inplace`` is called on an object, the :meth:`_transform_inplace` method is called, passing in the transforms' :meth:`_apply` function. This allows for the object to define how it should transform itself. """
[docs] def _transform_inplace(self, transform): r""" Apply the given transform function to ``self`` inplace. Parameters ---------- transform : `function` Function that applies a transformation to the transformable object. Returns ------- transformed : ``type(self)`` The transformed object, having been transformed in place. """ raise NotImplementedError()
def _transform(self, transform): r""" Apply the :map:`Transform` given in a non destructive manner - returning the transformed object and leaving this object as it was. Parameters ---------- transform : `function` Function that applies a transformation to the transformable object. Returns ------- transformed : ``type(self)`` A copy of the object, transformed. """ copy_of_self = self.copy() # transform the copy destructively copy_of_self._transform_inplace(transform) return copy_of_self
from .alignment import Alignment from .composable import TransformChain, ComposableTransform, VComposable from .invertible import Invertible, VInvertible