Source code for pylablib.core.dataproc.transform
import numpy as np
import warnings
from ...core.utils.cext_tools import pyd_load_warn_msg
try:
from .ctransform import CLinear2DTransform # pylint: disable=no-name-in-module,unused-import
except ImportError as err:
warnings.warn(pyd_load_warn_msg.format(err))
from .ctransform_fallback import CLinear2DTransform # pylint: disable=unused-import
[docs]
class LinearTransform:
"""
A generic linear transform which combines an affine transform with a given matrix and an additional shift.
Args:
tmatr: translational matrix (if ``None``, use a unity matrix)
shift: added shift (if ``None``, use a zero shift)
ndim: if both `tmatr` and `shift` are ``None``, specifies the dimensionality of the transform; otherwise, ignored
"""
def __init__(self, tmatr=None, shift=None, ndim=2):
tmatr=np.asarray(tmatr) if tmatr is not None else None
shift=np.asarray(shift) if shift is not None else None
if tmatr is not None:
ndim=tmatr.shape[0]
elif shift is not None:
ndim=shift.shape[0]
self.tmatr=tmatr if tmatr is not None else self._geteye(ndim)
self.shift=shift if shift is not None else self._getzeros(ndim)
if self.tmatr.ndim!=2 or self.tmatr.shape[0]!=self.tmatr.shape[1]:
raise ValueError("transformation matrix should be a 2D square array")
if self.shift.ndim!=1:
raise ValueError("shift should be a 1D array")
if self.tmatr.shape[0]!=self.shift.shape[0]:
raise ValueError("transformation matrix and shift should have the same size")
_eyes=[np.eye(d) for d in range(10)]
_zeros=[np.zeros(d) for d in range(10)]
def _geteye(self, d):
return np.eye(d) if d>=10 else self._eyes[d]
def _getzeros(self, d):
return np.zeros(d) if d>=10 else self._zeros[d]
def _build_new(self, tmatr, shift):
return type(self)(tmatr,shift)
def __call__(self, coord, shift=True):
coord=np.asarray(coord)
return np.dot(self.tmatr,coord)+self.shift if shift else np.dot(self.tmatr,coord)
[docs]
def i(self, coord, shift=True):
tmatr=np.linalg.inv(self.tmatr)
return np.dot(tmatr,coord)-np.dot(tmatr,self.shift) if shift else np.dot(tmatr,coord)
def _check_ndim(self, ndims):
if self.tmatr.shape[0] not in ndims:
raise ValueError("method only applies to dimensions {}".format(ndims))
[docs]
def inverted(self):
"""Return inverted transformation"""
tmatr=np.linalg.inv(self.tmatr)
shift=-np.dot(tmatr,self.shift)
return self._build_new(tmatr,shift)
[docs]
def preceded(self, trans):
"""Return a combined transformation which result from applying this transformation followed by `trans`"""
return trans.followed(self)
[docs]
def followed(self, trans):
"""Return a combined transformation which result from applying `trans` followed by this transformation"""
tmatr=np.dot(trans.tmatr,self.tmatr)
shift=trans.shift+np.dot(trans.tmatr,self.shift)
return self._build_new(tmatr,shift)
def _combined(self, trans, preceded):
return trans.followed(self) if preceded else self.followed(trans)
[docs]
def shifted(self, shift, preceded=False):
"""Return a transform with an added shift before or after (depending of `preceded`) the current one"""
return self._combined(LinearTransform(shift=shift),preceded)
[docs]
def multiplied(self, mult, preceded=False):
"""
Return a transform with an added scaling before or after (depending of `preceded`) the current one.
`mult` can be a single number (scale), a 1D vector (scaling for each axis independently), or a matrix.
"""
if np.ndim(mult)==0:
tmatr=self._geteye(self.tmatr.shape[0])*mult
elif np.ndim(mult)==1:
tmatr=np.diag(mult)
else:
tmatr=np.asarray(mult)
return self._combined(LinearTransform(tmatr),preceded)
[docs]
def rotated2d(self, deg, preceded=False):
"""
Return a transform with an added rotation before or after (depending of `preceded`) the current one.
Only applies to 2D transforms.
"""
self._check_ndim(2)
rad=deg*np.pi/180
tmatr=[[np.cos(rad),-np.sin(rad)],[np.sin(rad),np.cos(rad)]]
return self.multiplied(tmatr,preceded=preceded)
[docs]
class Indexed2DTransform(LinearTransform):
"""
A restriction of :class:`LinearTransform` which only applies to 2D and only allows rotations by multiples of 90 degrees.
Args:
tmatr: translational matrix (if ``None``, use a unity matrix)
shift: added shift (if ``None``, use a zero shift)
rigid: if ``True``, only allow orthogonal transforms, i.e., no scaling
"""
def __init__(self, tmatr=None, shift=None, rigid=False):
super().__init__(tmatr=tmatr,shift=shift,ndim=2)
self.rigid=rigid
self._check_tmatr()
def _check_tmatr(self):
if self.tmatr.shape[0]!=2:
raise ValueError("only 2D transform are allowed")
if not (self.tmatr[0,0]==self.tmatr[1,1]==0 or self.tmatr[0,1]==self.tmatr[1,0]==0):
raise ValueError("only 90 degree rotations and inversions are allowed")
if self.rigid:
pmatr=np.dot(self.tmatr.T,self.tmatr)
if not np.all(pmatr==np.eye(2)):
raise ValueError("only orthogonal transformations are allowed")
def _build_new(self, tmatr, shift):
return type(self)(tmatr,shift,rigid=self.rigid)
[docs]
def rotated2d(self, deg, preceded=False):
deg=deg%360
if deg%90!=0:
raise ValueError("only 90 degree rotations are allowed")
if deg==0:
tmatr=[[1,0],[0,1]]
elif deg==90:
tmatr=[[0,-1],[1,0]]
elif deg==180:
tmatr=[[-1,0],[0,-1]]
else:
tmatr=[[0,1],[-1,0]]
return self.multiplied(tmatr,preceded=preceded)