Source code for navis.transforms.thinplate

#    This script is part of navis (http://www.github.com/navis-org/navis).
#    Copyright (C) 2018 Philipp Schlegel
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.

"""Functions to perform thin plate spline transforms. Requires morphops."""

import morphops as mops
import numpy as np
import pandas as pd

from scipy.spatial.distance import cdist

from .base import BaseTransform


def distance_matrix(X,Y):
    """For (p1,k)-shaped X and (p2,k)-shaped Y, returns the (p1,p2) matrix
    where the element at [i,j] is the distance between X[i,:] and Y[j,:].

    This is a re-implementation of a morphops function using scipy to speed
    things up ~4 orders of magnitude.
    """
    return cdist(X, Y)

# Replace morphops's original slow distance_matrix function
mops.lmk_util.distance_matrix = distance_matrix


[docs] class TPStransform(BaseTransform): """Thin Plate Spline transforms of 3D spatial data. Parameters ---------- landmarks_source : (M, 3) numpy array Source landmarks as x/y/z coordinates. landmarks_target : (M, 3) numpy array Target landmarks as x/y/z coordinates. Examples -------- >>> from navis import transforms >>> import numpy as np >>> # Generate some mock landmarks >>> src = np.array([[0, 0, 0], [10, 10, 10], [100, 100, 100], [80, 10, 30]]) >>> trg = np.array([[1, 15, 5], [9, 18, 21], [80, 99, 120], [5, 10, 80]]) >>> tr = transforms.thinplate.TPStransform(src, trg) >>> points = np.array([[0, 0, 0], [50, 50, 50]]) >>> tr.xform(points) array([[ 1. , 15. , 5. ], [40.55555556, 54. , 65. ]]) """
[docs] def __init__(self, landmarks_source: np.ndarray, landmarks_target: np.ndarray): """Initialize class.""" # Some checks self.source = np.asarray(landmarks_source) self.target = np.asarray(landmarks_target) if self.source.shape[1] != 3: raise ValueError(f'Expected (N, 3) array, got {self.source.shape}') if self.target.shape[1] != 3: raise ValueError(f'Expected (N, 3) array, got {self.target.shape}') if self.source.shape[0] != self.target.shape[0]: raise ValueError('Number of source landmarks must match number of ' 'target landmarks.') self._W, self._A = None, None
def __eq__(self, other) -> bool: """Implement equality comparison.""" if isinstance(other, TPStransform): if self.source.shape[0] == other.source.shape[0]: if np.all(self.source == other.source): if np.all(self.target == other.target): return True return False def __neg__(self) -> 'TPStransform': """Invert direction.""" # Switch source and target return TPStransform(self.target, self.source) def _calc_tps_coefs(self): # Calculate thinplate coefficients self._W, self._A = mops.tps_coefs(self.source, self.target) @property def W(self): if isinstance(self._W, type(None)): # Calculate coefficients self._calc_tps_coefs() return self._W @property def A(self): if isinstance(self._A, type(None)): # Calculate coefficients self._calc_tps_coefs() return self._A def copy(self): """Make copy.""" x = TPStransform(self.source, self.target) x.__dict__.update(self.__dict__) return x def xform(self, points: np.ndarray) -> np.ndarray: """Transform points. Parameters ---------- points : (N, 3) array Points to transform. Returns ------- pointsxf : (N, 3) array Transformed points. """ if isinstance(points, pd.DataFrame): if any([c not in points for c in ['x', 'y', 'z']]): raise ValueError('DataFrame must have x/y/z columns.') points = points[['x', 'y', 'z']].values U = mops.K_matrix(points, self.source) P = mops.P_matrix(points) # The warped pts are the affine part + the non-uniform part return np.matmul(P, self.A) + np.matmul(U, self.W)