Source code for menpo.transform.thinplatesplines

import numpy as np
from .base import Transform, Alignment, Invertible
from .rbf import R2LogR2RBF


# Note we inherit from Alignment first to get it's n_dims behavior
[docs]class ThinPlateSplines(Alignment, Transform, Invertible): r""" The thin plate splines (TPS) alignment between 2D `source` and `target` landmarks. ``kernel`` can be used to specify an alternative kernel function. If ``None`` is supplied, the :class:`R2LogR2RBF` kernel will be used. Parameters ---------- source : ``(N, 2)`` `ndarray` The source points to apply the tps from target : ``(N, 2)`` `ndarray` The target points to apply the tps to kernel : :class:`menpo.transform.rbf.RadialBasisFunction`, optional The kernel to apply. min_singular_val : `float`, optional If the target has points that are nearly coincident, the coefficients matrix is rank deficient, and therefore not invertible. Therefore, we only take the inverse on the full-rank matrix and drop any singular values that are less than this value (close to zero). Raises ------ ValueError TPS is only with on 2-dimensional data """ def __init__(self, source, target, kernel=None, min_singular_val=1e-4): Alignment.__init__(self, source, target) if self.n_dims != 2: raise ValueError('TPS can only be used on 2D data.') if kernel is None: kernel = R2LogR2RBF(source.points) self.min_singular_val = min_singular_val self.kernel = kernel # k[i, j] is the rbf weighting between source i and j # (of course, k is thus symmetrical and it's diagonal nil) self.k = self.kernel.apply(self.source.points) # p is a homogeneous version of the source points self.p = np.concatenate( [np.ones([self.n_points, 1]), self.source.points], axis=1) o = np.zeros([3, 3]) top_l = np.concatenate([self.k, self.p], axis=1) bot_l = np.concatenate([self.p.T, o], axis=1) self.l = np.concatenate([top_l, bot_l], axis=0) self.v, self.y, self.coefficients = None, None, None self._build_coefficients() def _build_coefficients(self): self.v = self.target.points.T.copy() self.y = np.hstack([self.v, np.zeros([2, 3])]) # If two points are coincident, or very close to being so, then the # matrix is rank deficient and thus not-invertible. Therefore, # only take the inverse on the full-rank set of indices. _u, _s, _v = np.linalg.svd(self.l) keep = _s.shape[0] - sum(_s < self.min_singular_val) inv_l = _u[:, :keep].dot(1.0 / _s[:keep, None] * _v[:keep, :]) self.coefficients = inv_l.dot(self.y.T) def _sync_state_from_target(self): # now the target is updated, we only have to rebuild the # coefficients. self._build_coefficients() def _apply(self, points, **kwargs): r""" Performs a TPS transform on the given points. Parameters ---------- points : ``(N, D)`` `ndarray` The points to transform. Returns ------- f : ``(N, D)`` `ndarray` The transformed points Raises ------ ValueError TPS can only be applied to 2D data. """ if points.shape[1] != self.n_dims: raise ValueError('TPS can only be applied to 2D data.') x = points[..., 0][:, None] y = points[..., 1][:, None] # calculate the affine coefficients of the warp # (C = Constant component, then X, Y respectively) c_affine_c = self.coefficients[-3] c_affine_x = self.coefficients[-2] c_affine_y = self.coefficients[-1] # the affine warp component f_affine = c_affine_c + c_affine_x * x + c_affine_y * y # calculate a distance matrix (for L2 Norm) between every source # and the target kernel_dist = self.kernel.apply(points) # grab the affine free components of the warp c_affine_free = self.coefficients[:-3] # build the affine free warp component f_affine_free = kernel_dist.dot(c_affine_free) return f_affine + f_affine_free @property def has_true_inverse(self): r""" :type: ``False`` """ return False
[docs] def pseudoinverse(self): r""" The pseudoinverse of the transform - that is, the transform that results from swapping `source` and `target`, or more formally, negating the transforms parameters. If the transform has a true inverse this is returned instead. :type: ``type(self)`` """ return ThinPlateSplines(self.target, self.source, kernel=self.kernel)