Source code for menpo.transform.homogeneous.scale
import numpy as np
from .base import HomogFamilyAlignment
from .affine import DiscreteAffine, Affine
from .similarity import Similarity
[docs]def Scale(scale_factor, n_dims=None):
r"""
Factory function for producing Scale transforms. Zero scale factors are not
permitted.
A :class:`UniformScale` will be produced if:
- A `float` ``scale_factor`` and a ``n_dims`` `kwarg` are provided
- A `ndarray` ``scale_factor`` with shape ``(n_dims,)`` is provided
with all elements being the same
A :class:`NonUniformScale` will be provided if:
- A `ndarray` ``scale_factor`` with shape ``(n_dims,)`` is provided with
at least two differing scale factors.
Parameters
----------
scale_factor : `float` or ``(n_dims,)`` `ndarray`
Scale for each axis.
n_dims : `int`, optional
The dimensionality of the output transform.
Returns
-------
scale : :class:`UniformScale` or :class:`NonUniformScale`
The correct type of scale
Raises
------
ValueError
If any of the scale factors is zero
"""
from numbers import Number
if not isinstance(scale_factor, Number):
# some array like thing - make it a numpy array for sure
scale_factor = np.asarray(scale_factor)
if not np.all(scale_factor):
raise ValueError("Having a zero in one of the scales is invalid")
if n_dims is None:
# scale_factor better be a numpy array then
if np.allclose(scale_factor, scale_factor[0]):
return UniformScale(scale_factor[0], scale_factor.shape[0])
else:
return NonUniformScale(scale_factor)
else:
# interpret as a scalar then
return UniformScale(scale_factor, n_dims)
[docs]class NonUniformScale(DiscreteAffine, Affine):
r"""
An ``n_dims`` scale transform, with a scale component for each dimension.
Parameters
----------
scale : ``(n_dims,)`` `ndarray`
A scale for each axis.
skip_checks : `bool`, optional
If ``True`` avoid sanity checks on ``h_matrix`` for performance.
"""
def __init__(self, scale, skip_checks=False):
scale = np.asarray(scale)
if not skip_checks:
if scale.size > 3 or scale.size < 2:
raise ValueError(
"NonUniformScale can only be 2D or 3D" ", not {}".format(scale.size)
)
h_matrix = np.eye(scale.size + 1)
np.fill_diagonal(h_matrix, scale)
h_matrix[-1, -1] = 1
Affine.__init__(self, h_matrix, skip_checks=True, copy=False)
[docs] @classmethod
def init_identity(cls, n_dims):
r"""
Creates an identity transform.
Parameters
----------
n_dims : `int`
The number of dimensions.
Returns
-------
identity : :class:`NonUniformScale`
The identity matrix transform.
"""
return NonUniformScale(np.ones(n_dims))
@property
def scale(self):
r"""
The scale vector.
:type: ``(n_dims,)`` `ndarray`
"""
# Copy the vector as Numpy 1.10 will return a writeable view
return self.h_matrix.diagonal()[:-1].copy()
def _transform_str(self):
message = "NonUniformScale by {}".format(self.scale)
return message
@property
def n_parameters(self):
"""
The number of parameters: ``n_dims``. They have the form
``[scale_x, scale_y, ....]`` representing the scale across each axis.
:type: `list` of `int`
"""
return self.scale.size
def _as_vector(self):
r"""
Return the parameters of the transform as a 1D array. These parameters
are parametrised as deltas from the identity warp. The parameters
are output in the order ``[s0, s1, ...]``.
+----------+--------------------------------------------+
|parameter | definition |
+==========+============================================+
|s0 | The scale across the first axis |
+----------+--------------------------------------------+
|s1 | The scale across the second axis |
+----------+--------------------------------------------+
|... | ... |
+----------+--------------------------------------------+
|sn | The scale across the nth axis |
+----------+--------------------------------------------+
Returns
-------
s : ``(n_dims,)`` `ndarray`
The scale across each axis.
"""
return self.scale
def _from_vector_inplace(self, vector):
r"""
Updates the :class:`NonUniformScale` inplace.
Parameters
----------
vector : ``(n_dims,)`` `ndarray`
The array of parameters.
"""
np.fill_diagonal(self.h_matrix, vector)
self.h_matrix[-1, -1] = 1
@property
def composes_inplace_with(self):
r"""
:class:`NonUniformScale` can swallow composition with any other
:class:`NonUniformScale` and :class:`UniformScale`.
"""
return NonUniformScale, UniformScale
[docs] def pseudoinverse(self):
"""
The inverse scale matrix.
:type: :class:`NonUniformScale`
"""
return NonUniformScale(1.0 / self.scale, skip_checks=True)
[docs]class UniformScale(DiscreteAffine, Similarity):
r"""
An abstract similarity scale transform, with a single scale component
applied to all dimensions. This is abstracted out to remove unnecessary
code duplication.
Parameters
----------
scale : ``(n_dims,)`` `ndarray`
A scale for each axis.
n_dims : `int`
The number of dimensions
skip_checks : `bool`, optional
If ``True`` avoid sanity checks on ``h_matrix`` for performance.
"""
def __init__(self, scale, n_dims, skip_checks=False):
if not skip_checks:
if n_dims > 3 or n_dims < 2:
raise ValueError(
"UniformScale can only be 2D or 3D" ", not {}".format(n_dims)
)
h_matrix = np.eye(n_dims + 1)
np.fill_diagonal(h_matrix, scale)
h_matrix[-1, -1] = 1
Similarity.__init__(self, h_matrix, copy=False, skip_checks=True)
[docs] @classmethod
def init_identity(cls, n_dims):
r"""
Creates an identity transform.
Parameters
----------
n_dims : `int`
The number of dimensions.
Returns
-------
identity : :class:`UniformScale`
The identity matrix transform.
"""
return UniformScale(1, n_dims)
@property
def scale(self):
r"""
The single scale value.
:type: `float`
"""
return self.h_matrix[0, 0]
def _transform_str(self):
message = "UniformScale by {}".format(self.scale)
return message
@property
def n_parameters(self):
r"""
The number of parameters: 1
:type: `int`
"""
return 1
def _as_vector(self):
r"""
Return the parameters of the transform as a 1D array. These parameters
are parametrised as deltas from the identity warp. The parameters
are output in the order ``[s]``.
+----------+--------------------------------+
|parameter | definition |
+==========+================================+
|s | The scale across each axis |
+----------+--------------------------------+
Returns
-------
s : `float`
The scale across each axis.
"""
return np.asarray(self.scale)
def _from_vector_inplace(self, p):
r"""
Returns an instance of the transform from the given parameters,
expected to be in Fortran ordering.
Parameters
----------
p : `float`
The parameter
"""
np.fill_diagonal(self.h_matrix, p)
self.h_matrix[-1, -1] = 1
@property
def composes_inplace_with(self):
r"""
:class:`UniformScale` can swallow composition with any other
:class:`UniformScale`.
"""
return UniformScale
[docs] def pseudoinverse(self):
r"""
The inverse scale.
:type: :class:`UniformScale`
"""
return UniformScale(1.0 / self.scale, self.n_dims, skip_checks=True)
[docs]class AlignmentUniformScale(HomogFamilyAlignment, UniformScale):
r"""
Constructs a :class:`UniformScale` by finding the optimal scale transform to
align `source` to `target`.
Parameters
----------
source : :map:`PointCloud`
The source pointcloud instance used in the alignment
target : :map:`PointCloud`
The target pointcloud instance used in the alignment
"""
def __init__(self, source, target):
HomogFamilyAlignment.__init__(self, source, target)
UniformScale.__init__(self, target.norm() / source.norm(), source.n_dims)
def _from_vector_inplace(self, p):
r"""
Returns an instance of the transform from the given parameters,
expected to be in Fortran ordering.
Parameters
----------
p : `float`
The parameter
"""
UniformScale._from_vector_inplace(self, p)
self._sync_target_from_state()
def _sync_state_from_target(self):
new_scale = self.target.norm() / self.source.norm()
np.fill_diagonal(self.h_matrix, new_scale)
self.h_matrix[-1, -1] = 1
[docs] def as_non_alignment(self):
r"""Returns a copy of this uniform scale without it's alignment nature.
Returns
-------
transform : :map:`UniformScale`
A version of this scale with the same transform behavior but
without the alignment logic.
"""
return UniformScale(self.scale, self.n_dims)