Source code for menpo.transform.base.composable
from menpo.transform.base import Transform
from functools import reduce
[docs]class ComposableTransform(Transform):
r"""
:map:`Transform` subclass that enables native composition, such that the
behavior of multiple :map:`Transform` s is composed together in a natural
way.
"""
@property
def composes_inplace_with(self):
r"""
The :map:`Transform` s that this transform composes inplace with
**natively** (i.e. no :map:`TransformChain` will be produced).
An attempt to compose inplace against any type that is not an instance
of this property on this class will result in an `Exception`.
:type: :map:`Transform` or `tuple` of :map:`Transform` s
"""
raise NotImplementedError()
@property
def composes_with(self):
r"""
The :map:`Transform` s that this transform composes with **natively**
(i.e. no :map:`TransformChain` will be produced).
If native composition is not possible, falls back to producing a
:map:`TransformChain`.
By default, this is the same list as :attr:`composes_inplace_with`.
:type: :map:`Transform` or `tuple` of :map:`Transform` s
"""
return self.composes_inplace_with
[docs] def compose_before(self, transform):
r"""
A :map:`Transform` 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.
An attempt is made to perform native composition, but will fall back
to a :map:`TransformChain` as a last resort. See :attr:`composes_with`
for a description of how the mode of composition is decided.
Parameters
----------
transform : :map:`Transform`
Transform to be applied **after** ``self``
Returns
-------
transform : :map:`Transform` or :map:`TransformChain`
If the composition was native, a single new :map:`Transform` will
be returned. If not, a :map:`TransformChain` is returned instead.
"""
if isinstance(transform, self.composes_with):
return self._compose_before(transform)
else:
# best we can do is a TransformChain, let Transform handle that.
return Transform.compose_before(self, transform)
[docs] def compose_after(self, transform):
r"""
A :map:`Transform` 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``.
An attempt is made to perform native composition, but will fall back
to a :map:`TransformChain` as a last resort. See :attr:`composes_with`
for a description of how the mode of composition is decided.
Parameters
----------
transform : :map:`Transform`
Transform to be applied **before** ``self``
Returns
-------
transform : :map:`Transform` or :map:`TransformChain`
If the composition was native, a single new :map:`Transform` will
be returned. If not, a :map:`TransformChain` is returned instead.
"""
if isinstance(transform, self.composes_with):
return self._compose_after(transform)
else:
# best we can do is a TransformChain, let Transform handle that.
return Transform.compose_after(self, transform)
[docs] def compose_before_inplace(self, transform):
r"""
Update ``self`` so that it represents **this** transform composed
**before** the given transform::
a_orig = a.copy()
a.compose_before_inplace(b)
a.apply(p) == b.apply(a_orig.apply(p))
``a`` is permanently altered to be the result of the composition.
``b`` is left unchanged.
Parameters
----------
transform : :attr:`composes_inplace_with`
Transform to be applied **after** ``self``
Raises
------
ValueError
If ``transform`` isn't an instance of :attr:`composes_inplace_with`
"""
if isinstance(transform, self.composes_inplace_with):
self._compose_before_inplace(transform)
else:
raise ValueError(
"{} can only compose inplace with {} - not "
"{}".format(type(self), self.composes_inplace_with,
type(transform)))
[docs] def compose_after_inplace(self, transform):
r"""
Update ``self`` so that it represents **this** transform composed
**after** the given transform::
a_orig = a.copy()
a.compose_after_inplace(b)
a.apply(p) == a_orig.apply(b.apply(p))
``a`` is permanently altered to be the result of the composition. ``b``
is left unchanged.
Parameters
----------
transform : :attr:`composes_inplace_with`
Transform to be applied **before** ``self``
Raises
------
ValueError
If ``transform`` isn't an instance of :attr:`composes_inplace_with`
"""
if isinstance(transform, self.composes_inplace_with):
self._compose_after_inplace(transform)
else:
raise ValueError(
"{} can only compose inplace with {} - not "
"{}".format(type(self), self.composes_inplace_with,
type(transform)))
def _compose_before(self, transform):
r"""
Naive implementation of composition, ``self.copy()`` and then
:meth:``compose_before_inplace``. Apply this transform **first**.
Parameters
----------
transform : :map:`ComposableTransform`
Transform to be applied **after** ``self``
Returns
-------
transform : :map:`ComposableTransform`
The resulting transform.
"""
# naive approach - copy followed by the inplace operation
self_copy = self.copy()
self_copy._compose_before_inplace(transform)
return self_copy
def _compose_after(self, transform):
r"""
Naive implementation of composition, ``self.copy()`` and then
:meth:``compose_after_inplace``. Apply this transform **second**.
Parameters
----------
transform : :map:`ComposableTransform`
Transform to be applied **before** ``self``
Returns
-------
transform : :map:`ComposableTransform`
The resulting transform.
"""
# naive approach - copy followed by the inplace operation
self_copy = self.copy()
self_copy._compose_after_inplace(transform)
return self_copy
[docs] def _compose_before_inplace(self, transform):
r"""
Specialised inplace composition. This should be overridden to provide
specific cases of composition as defined in
:attr:`composes_inplace_with`.
Parameters
----------
transform : :attr:`composes_inplace_with`
Transform to be applied **after** ``self``
"""
raise NotImplementedError()
[docs] def _compose_after_inplace(self, transform):
r"""
Specialised inplace composition. This should be overridden to provide
specific cases of composition as defined in
:attr:`composes_inplace_with`.
Parameters
----------
transform : :attr:`composes_inplace_with`
Transform to be applied **before** ``self``
"""
raise NotImplementedError()
[docs]class VComposable(object):
r"""
Mix-in for :map:`Vectorizable` :map:`ComposableTransform` s.
Use this mix-in with :map:`ComposableTransform` if the
:map:`ComposableTransform` in question is :map:`Vectorizable` as this adds
:meth:`from_vector` variants to the :map:`ComposableTransform` interface.
These can be tuned for performance.
"""
[docs] def compose_after_from_vector_inplace(self, vector):
r"""
Specialised inplace composition with a vector. This should be
overridden to provide specific cases of composition whereby the current
state of the transform can be derived purely from the provided vector.
Parameters
----------
vector : ``(n_parameters,)`` `ndarray`
Vector to update the transform state with.
"""
raise NotImplementedError()
[docs]class TransformChain(ComposableTransform):
r"""
A chain of transforms that can be efficiently applied one after the other.
This class is the natural product of composition. Note that objects may
know how to compose themselves more efficiently - such objects implement
the :map:`ComposableTransform` or :map:`VComposable` interfaces.
Parameters
----------
transforms : `list` of :map:`Transform`
The `list` of transforms to be applied. Note that the first transform
will be applied first - the result of which is fed into the second
transform and so on until the chain is exhausted.
"""
def __init__(self, transforms):
# TODO Should TransformChain copy on input?
self.transforms = transforms
def _apply(self, x, **kwargs):
r"""
Applies each of the transforms to the array ``x``, in order.
Parameters
----------
x : ``(n_points, n_dims)`` `ndarray`
The array to transform.
Returns
-------
transformed : ``(n_points, n_dims_output)`` `ndarray`
Transformed array having passed through the chain of transforms.
"""
return reduce(lambda x_i, tr: tr._apply(x_i), self.transforms, x)
@property
def composes_inplace_with(self):
r"""
The :map:`Transform` s that this transform composes inplace with
**natively** (i.e. no :map:`TransformChain` will be produced).
An attempt to compose inplace against any type that is not an instance
of this property on this class will result in an `Exception`.
:type: :map:`Transform` or `tuple` of :map:`Transform` s
"""
return Transform
def _compose_before_inplace(self, transform):
r"""
Specialised inplace composition. In this case we merely keep a `list`
of :map:`Transform` s to apply in order.
Parameters
----------
transform : :map:`ComposableTransform`
Transform to be applied **after** ``self``
"""
self.transforms.append(transform)
def _compose_after_inplace(self, transform):
r"""
Specialised inplace composition. In this case we merely keep a `list`
of :map:`Transform`s to apply in order.
Parameters
----------
transform : :map:`ComposableTransform`
Transform to be applied **before** ``self``
"""
self.transforms.insert(0, transform)