[docs]classTransform:"""Base transform class. Defaults to the identity transform. Parameters ---------- func : callable, Coords -> Coords A function converting an NxD array of coordinates to NxD'. name : string A string name for the transform. """changed=Signal()def__init__(self,func=tz.identity,inverse=None,name=None)->None:self.func=funcself._inverse_func=inverseself.name=nameself._cache_dict={}iffuncistz.identity:self._inverse_func=tz.identitydef__call__(self,coords):"""Transform input coordinates to output."""returnself.func(coords)@propertydefinverse(self)->'Transform':ifself._inverse_funcisNone:raiseValueError(trans._('Inverse function was not provided.',deferred=True))if'inverse'notinself._cache_dict:self._cache_dict['inverse']=Transform(self._inverse_func,self.func)returnself._cache_dict['inverse']
[docs]defcompose(self,transform:'Transform')->'Transform':"""Return the composite of this transform and the provided one."""returnTransformChain([self,transform])
[docs]defset_slice(self,axes:Sequence[int])->'Transform':"""Return a transform subset to the visible dimensions. Parameters ---------- axes : Sequence[int] Axes to subset the current transform with. Returns ------- Transform Resulting transform. """raiseNotImplementedError(trans._('Cannot subset arbitrary transforms.',deferred=True))
[docs]defexpand_dims(self,axes:Sequence[int])->'Transform':"""Return a transform with added axes for non-visible dimensions. Parameters ---------- axes : Sequence[int] Location of axes to expand the current transform with. Passing a list allows expansion to occur at specific locations and for expand_dims to be like an inverse to the set_slice method. Returns ------- Transform Resulting transform. """raiseNotImplementedError(trans._('Cannot subset arbitrary transforms.',deferred=True))
@propertydef_is_diagonal(self):"""Indicate when a transform does not mix or permute dimensions. Can be overridden in subclasses to enable performance optimizations that are specific to this case. """returnFalsedef_clean_cache(self):self._cache_dict.clear()self.changed.emit()
_T=TypeVar('_T',bound=Transform)
[docs]classTransformChain(EventedList[_T],Transform,Generic[_T]):def__init__(self,transforms:Optional[Iterable[Transform]]=None)->None:iftransformsisNone:transforms=[]super().__init__(data=transforms,basetype=Transform,lookup={str:lambdax:x.name},)# The above super().__init__() will not call Transform.__init__().# For that to work every __init__() called using super() needs to# in turn call super().__init__(). So we call it explicitly here.Transform.__init__(self)fortrinself:ifhasattr(tr,'changed'):tr.changed.connect(self._clean_cache)def__call__(self,coords):returntz.pipe(coords,*self)def__newlike__(self,iterable):returnTransformChain(iterable)@overloaddef__getitem__(self,key:int)->_T:...@overloaddef__getitem__(self,key:str)->_T:...@overloaddef__getitem__(self,key:slice)->'TransformChain[_T]':...def__getitem__(self,key):iff'getitem_{key}'notinself._cache_dict:self._cache_dict[f'getitem_{key}']=super().__getitem__(key)returnself._cache_dict[f'getitem_{key}']def__setitem__(self,key,value):ifkeyinselfandhasattr(self[key],'changed'):self[key].changed.disconnect(self._clean_cache)super().__setitem__(key,value)ifhasattr(value,'changed'):value.changed.connect(self._clean_cache)self._clean_cache()def__delitem__(self,key):val=self[key]ifhasattr(val,'changed'):val.changed.disconnect(self._clean_cache)super().__delitem__(key)self._clean_cache()@propertydefinverse(self)->'TransformChain':"""Return the inverse transform chain."""if'inverse'notinself._cache_dict:self._cache_dict['inverse']=TransformChain([tf.inversefortfinself[::-1]])returnself._cache_dict['inverse']@propertydef_is_diagonal(self):ifall(getattr(tf,'_is_diagonal',False)fortfinself):returnTruereturngetattr(self.simplified,'_is_diagonal',False)@propertydefsimplified(self)->_T:""" Return the composite of the transforms inside the transform chain. Raises ------ ValueError If the transform chain is empty. """iflen(self)==0:raiseValueError(trans._('Cannot simplify an empty transform chain.'))iflen(self)==1:returnself[0]if'simplified'notinself._cache_dict:self._cache_dict['simplified']=tz.pipe(self[0],*[tf.composefortfinself[1:]])returnself._cache_dict['simplified']
[docs]defset_slice(self,axes:Sequence[int])->'TransformChain':"""Return a transform chain subset to the visible dimensions. Parameters ---------- axes : Sequence[int] Axes to subset the current transform chain with. Returns ------- TransformChain Resulting transform chain. """returnTransformChain([tf.set_slice(axes)fortfinself])
[docs]defexpand_dims(self,axes:Sequence[int])->'TransformChain':"""Return a transform chain with added axes for non-visible dimensions. Parameters ---------- axes : Sequence[int] Location of axes to expand the current transform with. Passing a list allows expansion to occur at specific locations and for expand_dims to be like an inverse to the set_slice method. Returns ------- TransformChain Resulting transform chain. """returnTransformChain([tf.expand_dims(axes)fortfinself])
[docs]classScaleTranslate(Transform):"""n-dimensional scale and translation (shift) class. Scaling is always applied before translation. Parameters ---------- scale : 1-D array A 1-D array of factors to scale each axis by. Scale is broadcast to 1 in leading dimensions, so that, for example, a scale of [4, 18, 34] in 3D can be used as a scale of [1, 4, 18, 34] in 4D without modification. An empty translation vector implies no scaling. translate : 1-D array A 1-D array of factors to shift each axis by. Translation is broadcast to 0 in leading dimensions, so that, for example, a translation of [4, 18, 34] in 3D can be used as a translation of [0, 4, 18, 34] in 4D without modification. An empty translation vector implies no translation. name : string A string name for the transform. """def__init__(self,scale=(1.0,),translate=(0.0,),*,name=None)->None:super().__init__(name=name)iflen(scale)>len(translate):translate=[0]*(len(scale)-len(translate))+list(translate)iflen(translate)>len(scale):scale=[1]*(len(translate)-len(scale))+list(scale)self.scale=np.array(scale)self.translate=np.array(translate)def__call__(self,coords):coords=np.asarray(coords)append_first_axis=coords.ndim==1ifappend_first_axis:coords=coords[np.newaxis,:]coords_ndim=coords.shape[1]ifcoords_ndim==len(self.scale):scale=self.scaletranslate=self.translateelse:scale=np.concatenate(([1.0]*(coords_ndim-len(self.scale)),self.scale))translate=np.concatenate(([0.0]*(coords_ndim-len(self.translate)),self.translate))out=scale*coordsout+=translateifappend_first_axis:out=out[0]returnout@propertydefinverse(self)->'ScaleTranslate':"""Return the inverse transform."""returnScaleTranslate(1/self.scale,-1/self.scale*self.translate)
[docs]defcompose(self,transform:'Transform')->'Transform':"""Return the composite of this transform and the provided one."""ifnotisinstance(transform,ScaleTranslate):super().compose(transform)scale=self.scale*transform.scaletranslate=self.translate+self.scale*transform.translatereturnScaleTranslate(scale,translate)
[docs]defset_slice(self,axes:Sequence[int])->'ScaleTranslate':"""Return a transform subset to the visible dimensions. Parameters ---------- axes : Sequence[int] Axes to subset the current transform with. Returns ------- Transform Resulting transform. """returnScaleTranslate(self.scale[axes],self.translate[axes],name=self.name)
[docs]defexpand_dims(self,axes:Sequence[int])->'ScaleTranslate':"""Return a transform with added axes for non-visible dimensions. Parameters ---------- axes : Sequence[int] Location of axes to expand the current transform with. Passing a list allows expansion to occur at specific locations and for expand_dims to be like an inverse to the set_slice method. Returns ------- Transform Resulting transform. """n=len(axes)+len(self.scale)not_axes=[iforiinrange(n)ifinotinaxes]scale=np.ones(n)scale[not_axes]=self.scaletranslate=np.zeros(n)translate[not_axes]=self.translatereturnScaleTranslate(scale,translate,name=self.name)
@propertydef_is_diagonal(self):"""Indicate that this transform does not mix or permute dimensions."""returnTrue
[docs]classAffine(Transform):"""n-dimensional affine transformation class. The affine transform can be represented as a n+1 dimensional transformation matrix in homogeneous coordinates [1]_, an n dimensional matrix and a length n translation vector, or be composed and decomposed from scale, rotate, and shear transformations defined in the following order: rotate * shear * scale + translate The affine_matrix representation can be used for easy compatibility with other libraries that can generate affine transformations. Parameters ---------- rotate : float, 3-tuple of float, or n-D array. If a float convert into a 2D rotation matrix using that value as an angle. If 3-tuple convert into a 3D rotation matrix, using a yaw, pitch, roll convention. Otherwise assume an nD rotation. Angles are assumed to be in degrees. They can be converted from radians with np.degrees if needed. scale : 1-D array A 1-D array of factors to scale each axis by. Scale is broadcast to 1 in leading dimensions, so that, for example, a scale of [4, 18, 34] in 3D can be used as a scale of [1, 4, 18, 34] in 4D without modification. An empty translation vector implies no scaling. shear : 1-D array or n-D array Either a vector of upper triangular values, or an nD shear matrix with ones along the main diagonal. translate : 1-D array A 1-D array of factors to shift each axis by. Translation is broadcast to 0 in leading dimensions, so that, for example, a translation of [4, 18, 34] in 3D can be used as a translation of [0, 4, 18, 34] in 4D without modification. An empty translation vector implies no translation. linear_matrix : n-D array, optional (N, N) matrix with linear transform. If provided then scale, rotate, and shear values are ignored. affine_matrix : n-D array, optional (N+1, N+1) affine transformation matrix in homogeneous coordinates [1]_. The first (N, N) entries correspond to a linear transform and the final column is a length N translation vector and a 1 or a napari AffineTransform object. If provided then translate, scale, rotate, and shear values are ignored. ndim : int The dimensionality of the transform. If None, this is inferred from the other parameters. name : string A string name for the transform. References ---------- .. [1] https://en.wikipedia.org/wiki/Homogeneous_coordinates. """def__init__(self,scale=(1.0,1.0),translate=(0.0,0.0,),*,affine_matrix=None,axis_labels:Optional[Sequence[str]]=None,linear_matrix=None,name=None,ndim=None,rotate=None,shear=None,units:Optional[Sequence[Union[str,pint.Unit]]]=None,)->None:super().__init__(name=name)self._upper_triangular=TrueifndimisNone:ndim=infer_ndim(scale=scale,translate=translate,rotate=rotate,shear=shear)ifaffine_matrixisnotNone:linear_matrix=affine_matrix[:-1,:-1]translate=affine_matrix[:-1,-1]eliflinear_matrixisnotNone:linear_matrix=np.array(linear_matrix)else:ifrotateisNone:rotate=np.eye(ndim)ifshearisNone:shear=np.eye(ndim)else:ifnp.array(shear).ndim==2:ifis_matrix_triangular(shear):self._upper_triangular=is_matrix_upper_triangular(shear)else:raiseValueError(trans._('Only upper triangular or lower triangular matrices are accepted for shear, got {shear}. For other matrices, set the affine_matrix or linear_matrix directly.',deferred=True,shear=shear,))linear_matrix=compose_linear_matrix(rotate,scale,shear)ndim=max(ndim,linear_matrix.shape[0])self._linear_matrix=embed_in_identity_matrix(linear_matrix,ndim)self._translate=translate_to_vector(translate,ndim=ndim)self._axis_labels=tuple(f'axis {i}'foriinrange(-ndim,0))self._units=(pint.get_application_registry().pixel,)*ndimself.axis_labels=axis_labelsself.units=unitsdef__call__(self,coords):coords=np.asarray(coords)append_first_axis=coords.ndim==1ifappend_first_axis:coords=coords[np.newaxis,:]coords_ndim=coords.shape[1]padded_linear_matrix=embed_in_identity_matrix(self._linear_matrix,coords_ndim)translate=translate_to_vector(self._translate,ndim=coords_ndim)out=coords@padded_linear_matrix.Tout+=translateifappend_first_axis:out=out[0]returnout@propertydefndim(self)->int:"""Dimensionality of the transform."""returnself._linear_matrix.shape[0]@propertydefaxis_labels(self)->tuple[str,...]:"""tuple of axis labels for the layer."""returnself._axis_labels@axis_labels.setterdefaxis_labels(self,axis_labels:Optional[Sequence[str]])->None:ifaxis_labelsisNone:axis_labels=tuple(f'axis {i}'foriinrange(-self.ndim,0))iflen(axis_labels)!=self.ndim:raiseValueError(f'{axis_labels=} must have length ndim={self.ndim}.')axis_labels=tuple(axis_labels)self._axis_labels=axis_labels@propertydefunits(self)->tuple[pint.Unit,...]:"""List of units for the layer."""returnself._units@units.setterdefunits(self,units:Optional[Sequence[pint.Unit]])->None:units=get_units_from_name(units)ifunitsisNone:units=(pint.get_application_registry().pixel,)*self.ndimifisinstance(units,pint.Unit):units=(units,)*self.ndimiflen(units)!=self.ndim:raiseValueError(f'{units=} must have length ndim={self.ndim}.')self._units=units@propertydefscale(self)->npt.NDArray:"""Return the scale of the transform."""ifself._is_diagonal:returnnp.diag(self._linear_matrix)self._setup_decompose_linear_matrix_cache()returnself._cache_dict['decompose_linear_matrix'][1]@scale.setterdefscale(self,scale):"""Set the scale of the transform."""ifself._is_diagonal:scale=scale_to_vector(scale,ndim=self.ndim)foriinrange(len(scale)):self._linear_matrix[i,i]=scale[i]else:self._linear_matrix=compose_linear_matrix(self.rotate,scale,self._shear_cache)self._clean_cache()@propertydefphysical_scale(self)->tuple[pint.Quantity,...]:"""Return the scale of the transform, with units."""returntuple(np.multiply(self.scale,self.units))@propertydeftranslate(self)->npt.NDArray:"""Return the translation of the transform."""returnself._translate@translate.setterdeftranslate(self,translate):"""Set the translation of the transform."""self._translate=translate_to_vector(translate,ndim=self.ndim)self._clean_cache()def_setup_decompose_linear_matrix_cache(self):if'decompose_linear_matrix'inself._cache_dict:returnself._cache_dict['decompose_linear_matrix']=decompose_linear_matrix(self.linear_matrix,upper_triangular=self._upper_triangular)@propertydefrotate(self)->npt.NDArray:"""Return the rotation of the transform."""self._setup_decompose_linear_matrix_cache()returnself._cache_dict['decompose_linear_matrix'][0]@rotate.setterdefrotate(self,rotate):"""Set the rotation of the transform."""self._linear_matrix=compose_linear_matrix(rotate,self.scale,self._shear_cache)self._clean_cache()@propertydefshear(self)->npt.NDArray:"""Return the shear of the transform."""ifself._is_diagonal:returnnp.zeros((self.ndim,))self._setup_decompose_linear_matrix_cache()returnself._cache_dict['decompose_linear_matrix'][2]@shear.setterdefshear(self,shear):"""Set the shear of the transform."""shear=np.asarray(shear)ifshear.ndim==2:ifis_matrix_triangular(shear):self._upper_triangular=is_matrix_upper_triangular(shear)else:raiseValueError(trans._('Only upper triangular or lower triangular matrices are accepted for shear, got {shear}. For other matrices, set the affine_matrix or linear_matrix directly.',deferred=True,shear=shear,))else:self._upper_triangular=Trueself._linear_matrix=compose_linear_matrix(self.rotate,self.scale,shear)self._clean_cache()@propertydef_shear_cache(self):self._setup_decompose_linear_matrix_cache()returnself._cache_dict['decompose_linear_matrix'][2]@propertydeflinear_matrix(self)->npt.NDArray:"""Return the linear matrix of the transform."""returnself._linear_matrix@linear_matrix.setterdeflinear_matrix(self,linear_matrix):"""Set the linear matrix of the transform."""self._linear_matrix=embed_in_identity_matrix(linear_matrix,ndim=self.ndim)self._clean_cache()@propertydefaffine_matrix(self)->npt.NDArray:"""Return the affine matrix for the transform."""matrix=np.eye(self.ndim+1,self.ndim+1)matrix[:-1,:-1]=self._linear_matrixmatrix[:-1,-1]=self._translatereturnmatrix@affine_matrix.setterdefaffine_matrix(self,affine_matrix):"""Set the affine matrix for the transform."""self._linear_matrix=affine_matrix[:-1,:-1]self._translate=affine_matrix[:-1,-1]self._clean_cache()def__array__(self,*args,**kwargs):"""NumPy __array__ protocol to get the affine transform matrix."""returnself.affine_matrix@propertydefinverse(self)->'Affine':"""Return the inverse transform."""if'inverse'notinself._cache_dict:self._cache_dict['inverse']=Affine(affine_matrix=np.linalg.inv(self.affine_matrix))returnself._cache_dict['inverse']@overloaddefcompose(self,transform:'Affine')->'Affine':...@overloaddefcompose(self,transform:'Transform')->'Transform':...
[docs]defcompose(self,transform):"""Return the composite of this transform and the provided one."""ifnotisinstance(transform,Affine):returnsuper().compose(transform)affine_matrix=self.affine_matrix@transform.affine_matrixreturnAffine(affine_matrix=affine_matrix)
[docs]defset_slice(self,axes:Sequence[int])->'Affine':"""Return a transform subset to the visible dimensions. Parameters ---------- axes : Sequence[int] Axes to subset the current transform with. Returns ------- Affine Resulting transform. """axes=list(axes)ifself._is_diagonal:linear_matrix=np.diag(self.scale[axes])else:linear_matrix=self.linear_matrix[np.ix_(axes,axes)]units=[self.units[i]foriinaxes]axes_labels=[self.axis_labels[i]foriinaxes]returnAffine(linear_matrix=linear_matrix,translate=self.translate[axes],ndim=len(axes),name=self.name,units=units,axis_labels=axes_labels,)
[docs]defreplace_slice(self,axes:Sequence[int],transform:'Affine')->'Affine':"""Returns a transform where the transform at the indicated n dimensions is replaced with another n-dimensional transform Parameters ---------- axes : Sequence[int] Axes where the transform will be replaced transform : Affine The transform that will be inserted. Must have as many dimension as len(axes) Returns ------- Affine Resulting transform. """iflen(axes)!=transform.ndim:raiseValueError(trans._('Dimensionality of provided axes list and transform differ.',deferred=True,))linear_matrix=np.copy(self.linear_matrix)linear_matrix[np.ix_(axes,axes)]=transform.linear_matrixtranslate=np.copy(self.translate)translate[axes]=transform.translatereturnAffine(linear_matrix=linear_matrix,translate=translate,ndim=len(axes),name=self.name,)
[docs]defexpand_dims(self,axes:Sequence[int])->'Affine':"""Return a transform with added axes for non-visible dimensions. Parameters ---------- axes : Sequence[int] Location of axes to expand the current transform with. Passing a list allows expansion to occur at specific locations and for expand_dims to be like an inverse to the set_slice method. Returns ------- Transform Resulting transform. """n=len(axes)+len(self.scale)not_axes=[iforiinrange(n)ifinotinaxes]linear_matrix=np.eye(n)linear_matrix[np.ix_(not_axes,not_axes)]=self.linear_matrixtranslate=np.zeros(n)translate[not_axes]=self.translatereturnAffine(linear_matrix=linear_matrix,translate=translate,ndim=n,name=self.name,)
@propertydef_is_diagonal(self):"""Determine whether linear_matrix is diagonal up to some tolerance. Since only `self.linear_matrix` is checked, affines with a translation component can still be considered diagonal. """if'_is_diagonal'notinself._cache_dict:self._cache_dict['_is_diagonal']=is_diagonal(self.linear_matrix,tol=1e-8)returnself._cache_dict['_is_diagonal']
[docs]classCompositeAffine(Affine):"""n-dimensional affine transformation composed from more basic components. Composition is in the following order rotate * shear * scale + translate Parameters ---------- rotate : float, 3-tuple of float, or n-D array. If a float convert into a 2D rotation matrix using that value as an angle. If 3-tuple convert into a 3D rotation matrix, using a yaw, pitch, roll convention. Otherwise assume an nD rotation. Angles are assumed to be in degrees. They can be converted from radians with np.degrees if needed. scale : 1-D array A 1-D array of factors to scale each axis by. Scale is broadcast to 1 in leading dimensions, so that, for example, a scale of [4, 18, 34] in 3D can be used as a scale of [1, 4, 18, 34] in 4D without modification. An empty translation vector implies no scaling. shear : 1-D array or n-D array Either a vector of upper triangular values, or an nD shear matrix with ones along the main diagonal. translate : 1-D array A 1-D array of factors to shift each axis by. Translation is broadcast to 0 in leading dimensions, so that, for example, a translation of [4, 18, 34] in 3D can be used as a translation of [0, 4, 18, 34] in 4D without modification. An empty translation vector implies no translation. ndim : int The dimensionality of the transform. If None, this is inferred from the other parameters. name : string A string name for the transform. """def__init__(self,scale=(1,1),translate=(0,0),*,axis_labels=None,rotate=None,shear=None,ndim=None,name=None,units=None,)->None:super().__init__(scale,translate,axis_labels=axis_labels,rotate=rotate,shear=shear,ndim=ndim,name=name,units=units,)ifndimisNone:ndim=infer_ndim(scale=scale,translate=translate,rotate=rotate,shear=shear)self._translate=translate_to_vector(translate,ndim=ndim)self._scale=scale_to_vector(scale,ndim=ndim)self._rotate=rotate_to_matrix(rotate,ndim=ndim)self._shear=shear_to_matrix(shear,ndim=ndim)self._linear_matrix=self._make_linear_matrix()@propertydefscale(self)->npt.NDArray:"""Return the scale of the transform."""returnself._scale@scale.setterdefscale(self,scale):"""Set the scale of the transform."""self._scale=scale_to_vector(scale,ndim=self.ndim)self._linear_matrix=self._make_linear_matrix()self._clean_cache()@propertydefrotate(self)->npt.NDArray:"""Return the rotation of the transform."""returnself._rotate@rotate.setterdefrotate(self,rotate):"""Set the rotation of the transform."""self._rotate=rotate_to_matrix(rotate,ndim=self.ndim)self._linear_matrix=self._make_linear_matrix()self._clean_cache()@propertydefshear(self)->npt.NDArray:"""Return the shear of the transform."""return(self._shear[np.triu_indices(n=self.ndim,k=1)]ifis_matrix_upper_triangular(self._shear)elseself._shear)@shear.setterdefshear(self,shear):"""Set the shear of the transform."""self._shear=shear_to_matrix(shear,ndim=self.ndim)self._linear_matrix=self._make_linear_matrix()self._clean_cache()@propertydeflinear_matrix(self):returnsuper().linear_matrix@linear_matrix.setterdeflinear_matrix(self,linear_matrix):"""Setting the linear matrix of a CompositeAffine transform is not supported."""raiseNotImplementedError(trans._('linear_matrix cannot be set directly for a CompositeAffine transform',deferred=True,))@propertydefaffine_matrix(self):returnsuper().affine_matrix@affine_matrix.setterdefaffine_matrix(self,affine_matrix):"""Setting the affine matrix of a CompositeAffine transform is not supported."""raiseNotImplementedError(trans._('affine_matrix cannot be set directly for a CompositeAffine transform',deferred=True,))