From 5b3c2906e0592c13f5897ee4184caefc973a63ed Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Wed, 7 Feb 2024 16:53:17 -0500 Subject: [PATCH 1/7] reconstruct FrameImage, Affine, DeformField 1. FrameImage.transform() - remove deprecated 'affine' argument - it now accepts Affine/DeformField object, or deformation displacement field for backward compatibility - move the codes that applies Affine transform to the image to Affine.transform() 2. Affine.transform() - accepts additional arguments to transform an image - implement __transform_image(): the codes are used to be the logic in FrameImage.transform() applying Affine transform to the image - returns either Transformed N-D point array or Transformed image depending on input 3. DeformField - rename change_space() to convert() - rename apply() to transform(): the codes are borrowed from the logic in FrameImage.transform() applying deformation displacement field to the image --- surfa/image/framed.py | 34 +++++++++--- surfa/transform/affine.py | 98 +++++++++++++++++++++++++++++++--- surfa/transform/deformfield.py | 17 ++++-- 3 files changed, 132 insertions(+), 17 deletions(-) diff --git a/surfa/image/framed.py b/surfa/image/framed.py index c73a522..be2dfbf 100644 --- a/surfa/image/framed.py +++ b/surfa/image/framed.py @@ -383,7 +383,7 @@ def resample_like(self, target, method='linear', copy=True, fill=0): method=method, affine=affine.matrix, fill=fill) return self.new(interped, target_geom) - def transform(self, trf=None, method='linear', rotation='corner', resample=True, fill=0, affine=None): + def transform(self, trf=None, method='linear', rotation='corner', resample=True, fill=0): """ Apply an affine or non-linear transform. @@ -406,8 +406,6 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, be updated (this is not possible if a displacement field is provided). fill : scalar Fill value for out-of-bounds voxels. - affine : Affine - Deprecated. Use the `trf` argument instead. Returns ------- @@ -418,12 +416,33 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, raise NotImplementedError('transform() is not yet implemented for 2D data, ' 'contact andrew if you need this') - if affine is not None: - trf = affine - warnings.warn('The \'affine\' argument to transform() is deprecated. Just use ' - 'the first positional argument to specify a transform.', + image = self.copy() + transformer = trf + if isinstance(transformer, np.ndarray): + warnings.warn('The option to pass \'trf\' argument as a numpy array is deprecated. ' + 'Pass \'trf\' either an Affine or DeFormField object', DeprecationWarning, stacklevel=2) + from surfa.transform.deformfield import DeformField + print("FramedImage.transform: create DeformField object") + deformation = cast_image(trf, fallback_geom=self.geom) + image = image.resample_like(deformation) + transformer = DeformField(data=trf, + source=image.geom, + target=deformation.geom, + format=DeformField.Format.disp_crs) + + if isinstance(transformer, Affine): + print("FramedImage.transform: Affine.transform") + return transformer.transform(image, method, rotation, resample, fill) + elif isinstance(transformer, DeformField): + print("FramedImage.transform: DeformField.transform") + return transformer.transform(image, method, fill) + + + """ + the following codes have been moved to Affine.transform and DeformField.transform + # one of these two will be set by the end of the function disp_data = None matrix_data = None @@ -519,6 +538,7 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, disp=disp_data, fill=fill) return self.new(interpolated, target_geom) + """ def reorient(self, orientation, copy=True): """ diff --git a/surfa/transform/affine.py b/surfa/transform/affine.py index 6fb7609..003039d 100644 --- a/surfa/transform/affine.py +++ b/surfa/transform/affine.py @@ -201,9 +201,9 @@ def __call__(self, points): """ return self.transform(points) - def transform(self, points): + def transform(self, points, method='linear', rotation='corner', resample=True, fill=0): """ - Apply the affine transform matrix to an N-D point or set of points. + Apply the affine transform matrix to an N-D point (set of points), or an image. Parameters ---------- @@ -213,14 +213,18 @@ def transform(self, points): Returns ------- (..., N) float - Transformed N-D point array. + Transformed N-D point array if (input is N-D point) + + transformed : Volume + transformed image if (input is an image) """ - # a common mistake is to use this function for transforming an image - # or mesh, so run this check to help the user out a bit + # a common mistake is to use this function for transforming a mesh, + # so run this check to help the user out a bit if ismesh(points): raise ValueError('use mesh.transform(affine) to apply an affine to a mesh') + if isimage(points): - raise ValueError('use image.transform(affine) to apply an affine to an image') + return self.__transform_image(points, method, rotation, resample, fill) # convert to array points = np.ascontiguousarray(points) @@ -365,6 +369,88 @@ def convert(self, source=None, target=None, space=None, copy=True): return Affine(affine.matrix, source=source, target=target, space=space) + # the implementation is based on FramedImage.transform + def __transform_image(self, image, method='linear', rotation='corner', resample=True, fill=0): + """ + Apply the affine transform matrix to an image. + + Parameters + ---------- + image : Volume + input image Volume + + Returns + ------- + transformed : Volume + transformed image + """ + + if image.basedim == 2: + raise NotImplementedError('Affine.transform() is not yet implemented for 2D data') + + affine = self.copy() + + # if not resampling, just change the image vox2world matrix and return + if not resample: + + # TODO: if affine is missing geometry info, do we assume that the affine + # is in world space or voxel space? let's do world for now + if affine.source is not None and affine.target is not None: + affine = affine.convert(space='world', source=image) + # TODO: must try this again once I changed everything around!! + elif affine.space is None: + warnings.warn('Affine transform is missing metadata defining its coordinate ' + 'space or source and target geometry. Assuming matrix is a ' + 'world-space transform since resample=False, but this might ' + 'not always be the case. Best practice is to provide the ' + 'correct metadata in the affine') + elif affine.space != 'world': + raise ValueError('affine must contain source and target info ' + 'if not in world space') + + # apply forward transform to the header + transformed = image.copy() + transformed.geom.update(vox2world=affine @ affine.source.vox2world) + return transformed + + # sanity check and preprocess the affine if resampling + target_geom = image.geom + + if affine.source is not None and affine.target is not None: + # it should be assumed that the default affine space is voxel + # when both source and target are set + if affine.space is None: + affine = affine.copy() + affine.space = 'voxel' + # + affine = affine.convert(space='voxel', source=image) + target_geom = affine.target + elif affine.space is not None and affine.space != 'voxel': + raise ValueError('affine must contain source and target info if ' + 'coordinate space is not \'voxel\'') + + # ensure the rotation is around the image corner before interpolating + if rotation not in ('center', 'corner'): + raise ValueError("rotation must be 'center' or 'corner'") + elif rotation == 'center': + affine = center_to_corner_rotation(affine, image.baseshape) + + # make sure the matrix is actually inverted since we want a target to + # source voxel mapping for resampling + matrix_data = affine.inv().matrix + source_data = image.framed_data + + # do the interpolation + from surfa.image.interp import interpolate + interpolated = interpolate(source=source_data, + target_shape=target_geom.shape, + method=method, + affine=matrix_data, + fill=fill) + return image.new(interpolated, target_geom) + + + def affine_equal(a, b, matrix_only=False, tol=0.0): """ Test whether two affine transforms are equivalent. diff --git a/surfa/transform/deformfield.py b/surfa/transform/deformfield.py index 39b0de1..9695ae6 100644 --- a/surfa/transform/deformfield.py +++ b/surfa/transform/deformfield.py @@ -141,7 +141,7 @@ def save(self, filename): # # change deformation field data format # return new deformation field, self._data is not changed - def change_space(self, newformat=Format.abs_crs): + def convert(self, newformat=Format.abs_crs): """ Change deformation field data format @@ -247,7 +247,7 @@ def change_space(self, newformat=Format.abs_crs): # # apply _data on given image using Cython interpolation in image/interp.pyx # return transformed image - def apply(self, image, method='linear', fill=0): + def transform(self, image, method='linear', fill=0): """ Apply dense deformation field to input image volume @@ -264,14 +264,23 @@ def apply(self, image, method='linear', fill=0): # check if image is a Volume if (not isinstance(image, sf.image.framed.Volume)): - raise ValueError('DeformField::apply() - input is not a Volume') + raise ValueError('DeformField.transform() - input is not a Volume') + if image.basedim == 2: + raise NotImplementedError('DeformField.transform() is not yet implemented for 2D data') + + if self._data.shape[-1] != image.basedim: + raise ValueError(f'deformation ({self._data.shape[-1]}D) does not match ' + f'dimensionality of image ({image.basedim}D)') + + """ # get the image in the space of the deformation #source_data = image.resample_like(self._target).framed_data + """ source_data = image.framed_data # convert deformation field to disp_crs - deformationfield = self.change_space(self.Format.disp_crs) + deformationfield = self.convert(self.Format.disp_crs) # do the interpolation, the function assumes disp_crs deformation field interpolated = interpolate(source=source_data, From 16a48925d147b317e06f9c73028961d1f75616b3 Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Wed, 7 Feb 2024 16:59:34 -0500 Subject: [PATCH 2/7] remove debug output --- surfa/image/framed.py | 4 +--- surfa/transform/affine.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/surfa/image/framed.py b/surfa/image/framed.py index be2dfbf..f7134a1 100644 --- a/surfa/image/framed.py +++ b/surfa/image/framed.py @@ -424,7 +424,7 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, DeprecationWarning, stacklevel=2) from surfa.transform.deformfield import DeformField - print("FramedImage.transform: create DeformField object") + deformation = cast_image(trf, fallback_geom=self.geom) image = image.resample_like(deformation) transformer = DeformField(data=trf, @@ -433,10 +433,8 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, format=DeformField.Format.disp_crs) if isinstance(transformer, Affine): - print("FramedImage.transform: Affine.transform") return transformer.transform(image, method, rotation, resample, fill) elif isinstance(transformer, DeformField): - print("FramedImage.transform: DeformField.transform") return transformer.transform(image, method, fill) diff --git a/surfa/transform/affine.py b/surfa/transform/affine.py index 003039d..074c09d 100644 --- a/surfa/transform/affine.py +++ b/surfa/transform/affine.py @@ -216,7 +216,7 @@ def transform(self, points, method='linear', rotation='corner', resample=True, f Transformed N-D point array if (input is N-D point) transformed : Volume - transformed image if (input is an image) + Transformed image if (input is an image) """ # a common mistake is to use this function for transforming a mesh, # so run this check to help the user out a bit From 3e928768dc9d639715b2f2c93d962f7d41df4a36 Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Wed, 7 Feb 2024 18:17:51 -0500 Subject: [PATCH 3/7] fix UnboundLocalError FramedImage.transform(): import DeformField from surfa.transform.deformfield --- surfa/image/framed.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surfa/image/framed.py b/surfa/image/framed.py index f7134a1..d45e684 100644 --- a/surfa/image/framed.py +++ b/surfa/image/framed.py @@ -418,12 +418,12 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, image = self.copy() transformer = trf - if isinstance(transformer, np.ndarray): + + from surfa.transform.deformfield import DeformField + if isinstance(transformer, np.ndarray): warnings.warn('The option to pass \'trf\' argument as a numpy array is deprecated. ' 'Pass \'trf\' either an Affine or DeFormField object', DeprecationWarning, stacklevel=2) - - from surfa.transform.deformfield import DeformField deformation = cast_image(trf, fallback_geom=self.geom) image = image.resample_like(deformation) From 3d71beee56b1e659479be0a66be83dcaf8987d23 Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Thu, 8 Feb 2024 10:40:30 -0500 Subject: [PATCH 4/7] add check for FramedImage.transform() trf argument, import DeformField only if trf is not an Affine object --- surfa/image/framed.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/surfa/image/framed.py b/surfa/image/framed.py index d45e684..05ef8e9 100644 --- a/surfa/image/framed.py +++ b/surfa/image/framed.py @@ -418,11 +418,13 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, image = self.copy() transformer = trf - + if isinstance(transformer, Affine): + return transformer.transform(image, method, rotation, resample, fill) + from surfa.transform.deformfield import DeformField if isinstance(transformer, np.ndarray): warnings.warn('The option to pass \'trf\' argument as a numpy array is deprecated. ' - 'Pass \'trf\' either an Affine or DeFormField object', + 'Pass \'trf\' as either an Affine or DeFormField object', DeprecationWarning, stacklevel=2) deformation = cast_image(trf, fallback_geom=self.geom) @@ -431,11 +433,11 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, source=image.geom, target=deformation.geom, format=DeformField.Format.disp_crs) + + if not isinstance(transformer, DeformField): + raise ValueError("Pass \'trf\' as either an Affine or DeFormField object") - if isinstance(transformer, Affine): - return transformer.transform(image, method, rotation, resample, fill) - elif isinstance(transformer, DeformField): - return transformer.transform(image, method, fill) + return transformer.transform(image, method, fill) """ From 53894ab9bf806c94a0daed56eb730802a3f166af Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Fri, 9 Feb 2024 14:40:44 -0500 Subject: [PATCH 5/7] 1. more documentations 2. rename transform/deformfield.py -> transform/warp.py 3. add Warp.__call__ --- surfa/image/framed.py | 125 +++----------------- surfa/transform/__init__.py | 2 +- surfa/transform/affine.py | 50 ++++---- surfa/transform/{deformfield.py => warp.py} | 31 +++-- 4 files changed, 67 insertions(+), 141 deletions(-) rename surfa/transform/{deformfield.py => warp.py} (92%) diff --git a/surfa/image/framed.py b/surfa/image/framed.py index 05ef8e9..36f9a6d 100644 --- a/surfa/image/framed.py +++ b/surfa/image/framed.py @@ -387,14 +387,17 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, """ Apply an affine or non-linear transform. - **Note on deformation fields:** Until we come up with a reasonable way to represent - deformation fields, they can be implemented as multi-frame images. It is assumed that - they represent a *displacement* vector field in voxel space. So under the hood, images - will be moved into the space of the deformation field if the image geometries differ. + **The original implementation has been moved to Affine.transform and Warp.transform. + **The method is now impemeted to transform image using Affine.transform and Warp.transform. + + **Note on trf argument:** It accepts Affine/Warp object, or deformation fields (4D numpy array). + Pass trf argument as a numpy array is deprecated and will be removed in the future. + It is assumed that the deformation fields represent a *displacement* vector field in voxel space. + So under the hood, images will be moved into the space of the deformation field if the image geometries differ. Parameters ---------- - trf : Affine or !class + trf : Affine/Warp or !class Affine transform or nonlinear deformation (displacement) to apply to the image. method : {'linear', 'nearest'} Image interpolation method if resample is enabled. @@ -421,125 +424,25 @@ def transform(self, trf=None, method='linear', rotation='corner', resample=True, if isinstance(transformer, Affine): return transformer.transform(image, method, rotation, resample, fill) - from surfa.transform.deformfield import DeformField + from surfa.transform.warp import Warp if isinstance(transformer, np.ndarray): warnings.warn('The option to pass \'trf\' argument as a numpy array is deprecated. ' - 'Pass \'trf\' as either an Affine or DeFormField object', + 'Pass \'trf\' as either an Affine or Warp object', DeprecationWarning, stacklevel=2) deformation = cast_image(trf, fallback_geom=self.geom) image = image.resample_like(deformation) - transformer = DeformField(data=trf, + transformer = Warp(data=trf, source=image.geom, target=deformation.geom, - format=DeformField.Format.disp_crs) + format=Warp.Format.disp_crs) - if not isinstance(transformer, DeformField): - raise ValueError("Pass \'trf\' as either an Affine or DeFormField object") + if not isinstance(transformer, Warp): + raise ValueError("Pass \'trf\' as either an Affine or Warp object") return transformer.transform(image, method, fill) - """ - the following codes have been moved to Affine.transform and DeformField.transform - - # one of these two will be set by the end of the function - disp_data = None - matrix_data = None - - # first try to convert it to an affine matrix. if that fails - # we assume it has to be a deformation field - try: - trf = cast_affine(trf, allow_none=False) - except ValueError: - pass - - if isinstance(trf, Affine): - - # for clarity - affine = trf - - # if not resampling, just change the image vox2world matrix and return - if not resample: - - # TODO: if affine is missing geometry info, do we assume that the affine - # is in world space or voxel space? let's do world for now - if affine.source is not None and affine.target is not None: - affine = affine.convert(space='world', source=self) - # TODO: must try this again once I changed everything around!! - elif affine.space is None: - warnings.warn('Affine transform is missing metadata defining its coordinate ' - 'space or source and target geometry. Assuming matrix is a ' - 'world-space transform since resample=False, but this might ' - 'not always be the case. Best practice is to provide the ' - 'correct metadata in the affine') - elif affine.space != 'world': - raise ValueError('affine must contain source and target info ' - 'if not in world space') - - # apply forward transform to the header - transformed = self.copy() - transformed.geom.update(vox2world=affine @ affine.source.vox2world) - return transformed - - # sanity check and preprocess the affine if resampling - target_geom = self.geom - - if affine.source is not None and affine.target is not None: - # it should be assumed that the default affine space is voxel - # when both source and target are set - if affine.space is None: - affine = affine.copy() - affine.space = 'voxel' - # - affine = affine.convert(space='voxel', source=self) - target_geom = affine.target - elif affine.space is not None and affine.space != 'voxel': - raise ValueError('affine must contain source and target info if ' - 'coordinate space is not \'voxel\'') - - # ensure the rotation is around the image corner before interpolating - if rotation not in ('center', 'corner'): - raise ValueError("rotation must be 'center' or 'corner'") - elif rotation == 'center': - affine = center_to_corner_rotation(affine, self.baseshape) - - # make sure the matrix is actually inverted since we want a target to - # source voxel mapping for resampling - matrix_data = affine.inv().matrix - source_data = self.framed_data - - else: - if not resample: - raise ValueError('transform resampling must be enabled when deformation is used') - - # cast deformation as a framed image data. important that the fallback geometry - # here is the current image space - deformation = cast_image(trf, fallback_geom=self.geom) - if deformation.nframes != self.basedim: - raise ValueError(f'deformation ({deformation.nframes}D) does not match ' - f'dimensionality of image ({self.basedim}D)') - - # since we only support deformations in the form of voxel displacement - # currently, must get the image in the space of the deformation - source_data = self.resample_like(deformation).framed_data - - # make sure to use the deformation as the target geometry - target_geom = deformation.geom - - # get displacement data - disp_data = deformation.data - - # do the interpolation - interpolated = interpolate(source=source_data, - target_shape=target_geom.shape, - method=method, - affine=matrix_data, - disp=disp_data, - fill=fill) - return self.new(interpolated, target_geom) - """ - def reorient(self, orientation, copy=True): """ Realigns image data and world matrix to conform to a specific slice orientation. diff --git a/surfa/transform/__init__.py b/surfa/transform/__init__.py index edbbf10..4534062 100644 --- a/surfa/transform/__init__.py +++ b/surfa/transform/__init__.py @@ -14,4 +14,4 @@ from .geometry import image_geometry2volgeom_dict from .geometry import volgeom_dict2image_geometry -from .deformfield import DeformField +from .warp import Warp diff --git a/surfa/transform/affine.py b/surfa/transform/affine.py index 074c09d..607ef16 100644 --- a/surfa/transform/affine.py +++ b/surfa/transform/affine.py @@ -194,52 +194,62 @@ def __matmul__(self, other): matrix = np.matmul(self.matrix, other.matrix) return Affine(matrix) - def __call__(self, points): + def __call__(self, *args, **kwargs): """ - Apply the affine transform matrix to a set of points. Calls `self.transform(points)` - under the hood. + Apply the affine transform matrix to a set of points, or an image Volume. + Calls `self.transform()` under the hood. """ - return self.transform(points) + return self.transform(*args, **kwargs) - def transform(self, points, method='linear', rotation='corner', resample=True, fill=0): + def transform(self, data, method='linear', rotation='corner', resample=True, fill=0): """ - Apply the affine transform matrix to an N-D point (set of points), or an image. + Apply the affine transform matrix to the input data. Parameters ---------- - points : (..., N) float - N-D point values to transform. + data : input data to transform + N-D point values, or image Volume + method : {'linear', 'nearest'} + Image interpolation method if resample is enabled. + rotation : {'corner', 'center'} + Apply affine with rotation axis at the image corner or center. + resample : bool + If enabled, voxel data will be interpolated and resampled, and geometry will be set + the target. If disabled, voxel data will not be modified, and only the geometry will + be updated (this is not possible if a displacement field is provided). + fill : scalar + Fill value for out-of-bounds voxels. Returns ------- (..., N) float - Transformed N-D point array if (input is N-D point) + Transformed N-D point array if (input data is N-D point) transformed : Volume - Transformed image if (input is an image) + Transformed image if (input data is an image Volume) """ # a common mistake is to use this function for transforming a mesh, # so run this check to help the user out a bit - if ismesh(points): + if ismesh(data): raise ValueError('use mesh.transform(affine) to apply an affine to a mesh') - if isimage(points): - return self.__transform_image(points, method, rotation, resample, fill) + if isimage(data): + return self.__transform_image(data, method, rotation, resample, fill) # convert to array - points = np.ascontiguousarray(points) + data = np.ascontiguousarray(data) # check correct dimensionality - if points.shape[-1] != self.ndim: + if data.shape[-1] != self.ndim: raise ValueError(f'transform() method expected {self.ndim}D points, but got ' - f'{points.shape[-1]}D input with shape {points.shape}') + f'{data.shape[-1]}D input with shape {data.shape}') # account for multiple possible input axes and be sure to # always return the same shape - shape = points.shape - points = points.reshape(-1, self.ndim) - points = np.c_[points, np.ones(points.shape[0])].T - moved = np.dot(self.matrix, points).T[:, :-1] + shape = data.shape + data = data.reshape(-1, self.ndim) + data = np.c_[data, np.ones(data.shape[0])].T + moved = np.dot(self.matrix, data).T[:, :-1] return np.ascontiguousarray(moved).reshape(shape) def inv(self): diff --git a/surfa/transform/deformfield.py b/surfa/transform/warp.py similarity index 92% rename from surfa/transform/deformfield.py rename to surfa/transform/warp.py index 9695ae6..00f0c8d 100644 --- a/surfa/transform/deformfield.py +++ b/surfa/transform/warp.py @@ -6,7 +6,7 @@ from surfa.image.interp import interpolate -class DeformField: +class Warp: class Format: """ @@ -41,7 +41,7 @@ def __init__(self, data=None, source=None, target=None, RAS ABS coordinate Y, or RAS DISP coordinate Y frame 2 - image voxel ABS coordinate S, image voxel DISP coordinate S, RAS ABS coordinate Z, or RAS DISP coordinate Z - _format: DeformField.Format + _format: Warp.Format _source: ImageGeometry, source image _target: ImageGeometry, target image _spacing: int (this is from m3z, not sure if it is really needed) @@ -71,9 +71,18 @@ def __init__(self, data=None, source=None, target=None, self._spacing = spacing self._exp_k = exp_k else: - raise ValueError('DeformField constructor: input parameters error') + raise ValueError('Warp constructor: input parameters error') + # + def __call__(self, *args, **kwargs): + """ + Apply non-linear transform to an image. + Calls `self.transform()` under the hood. + """ + return self.transform(*args, **kwargs) + + # # Read input mgz warp file def load(self, filename): @@ -90,11 +99,11 @@ def load(self, filename): # check if mgzwarp is a volume if (not isinstance(mgzwarp, sf.image.framed.Volume)): - raise ValueError('DeformField::load() - input is not a Volume') + raise ValueError('Warp::load() - input is not a Volume') # check if input is a mgzwarp (intent FramedArrayIntents.warpmap) if (mgzwarp.metadata['intent'] != sf.core.framed.FramedArrayIntents.warpmap): - raise ValueError('DeformField::load() - input is not a mgzwarp Volume') + raise ValueError('Warp::load() - input is not a mgzwarp Volume') self._data = mgzwarp.data self._format = mgzwarp.metadata['warpfield_dtfmt'] @@ -255,6 +264,10 @@ def transform(self, image, method='linear', fill=0): ---------- image : Volume input image Volume + method : {'linear', 'nearest'} + Image interpolation method + fill : scalar + Fill value for out-of-bounds voxels. Returns ------- @@ -264,10 +277,10 @@ def transform(self, image, method='linear', fill=0): # check if image is a Volume if (not isinstance(image, sf.image.framed.Volume)): - raise ValueError('DeformField.transform() - input is not a Volume') + raise ValueError('Warp.transform() - input is not a Volume') if image.basedim == 2: - raise NotImplementedError('DeformField.transform() is not yet implemented for 2D data') + raise NotImplementedError('Warp.transform() is not yet implemented for 2D data') if self._data.shape[-1] != image.basedim: raise ValueError(f'deformation ({self._data.shape[-1]}D) does not match ' @@ -300,8 +313,8 @@ def transform(self, image, method='linear', fill=0): def data(self): return self._data @data.setter - def data(self, deformfield): - self._data = deformfield + def data(self, warp): + self._data = warp # From 9cfee81a64a46651daf0739e95261b86407614d9 Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Fri, 9 Feb 2024 15:18:24 -0500 Subject: [PATCH 6/7] for backward compatibility, add `points` as a named argument with default value None to Affine.transform. Issue deprecated warning if it is not None. --- surfa/transform/affine.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/surfa/transform/affine.py b/surfa/transform/affine.py index 607ef16..b2b09ea 100644 --- a/surfa/transform/affine.py +++ b/surfa/transform/affine.py @@ -201,7 +201,7 @@ def __call__(self, *args, **kwargs): """ return self.transform(*args, **kwargs) - def transform(self, data, method='linear', rotation='corner', resample=True, fill=0): + def transform(self, data, method='linear', rotation='corner', resample=True, fill=0, points=None): """ Apply the affine transform matrix to the input data. @@ -228,6 +228,12 @@ def transform(self, data, method='linear', rotation='corner', resample=True, fil transformed : Volume Transformed image if (input data is an image Volume) """ + if points is not None: + data = points + warnings.warn('The \'points\' argument to transform() is deprecated. Just use ' + 'the first positional argument to specify set of points or an image to transform.', + DeprecationWarning, stacklevel=2) + # a common mistake is to use this function for transforming a mesh, # so run this check to help the user out a bit if ismesh(data): From 1622283bc4d3b9f6d462544ef79343fe7a5174cb Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Fri, 9 Feb 2024 15:27:55 -0500 Subject: [PATCH 7/7] add comment in docstring --- surfa/transform/affine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/surfa/transform/affine.py b/surfa/transform/affine.py index b2b09ea..08f03a2 100644 --- a/surfa/transform/affine.py +++ b/surfa/transform/affine.py @@ -219,6 +219,8 @@ def transform(self, data, method='linear', rotation='corner', resample=True, fil be updated (this is not possible if a displacement field is provided). fill : scalar Fill value for out-of-bounds voxels. + points : N-D point values + Deprecated. Use the `data` argument instead. Returns -------