8000 ENH,WIP: New transforms module by oesteban · Pull Request #656 · nipy/nibabel · GitHub
[go: up one dir, main page]

Skip to content

ENH,WIP: New transforms module #656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3cfff0d
ENH: Add an utility to calculate obliquity of affines
oesteban Sep 20, 2019
c92d560
enh(tests): add not-oblique test, move tests to ``test_affines.py``
oesteban Sep 20, 2019
da8da56
enh: return radians unless degrees=True
oesteban Sep 22, 2019
253a256
enh: address @matthew-brett's comments
oesteban Sep 23, 2019
04584b6
doc: add link to AFNI's documentation about *obliquity* [skip ci]
oesteban Sep 24, 2019
70d9620
Initial commit
oesteban Aug 1, 2018
36a1764
add TransformBase
oesteban Aug 1, 2018
2a700c9
add new module
oesteban Aug 1, 2018
fda2bfe
add documentation, core functionality
oesteban Aug 1, 2018
3ef5adc
add a deformation field transform
oesteban Aug 1, 2018
189a662
optimize deformation field
oesteban Aug 2, 2018
2619ae4
pre-cache transformed indexes
oesteban Aug 2, 2018
4e7c19b
used cached field
oesteban Aug 2, 2018
23d7002
add caching of deltas in voxel coordinates
oesteban Aug 3, 2018
2acd9f3
add bspline cython extension
oesteban Aug 3, 2018
385ceff
a smarter ImageSpace
oesteban Aug 4, 2018
fa030a4
add comment
oesteban Aug 10, 2018
fd418fe
remove cython module
oesteban Aug 10, 2018
13d722f
cleanup
oesteban Aug 11, 2018
253aed5
starting with bspline transform
oesteban Aug 11, 2018
47c2a57
finishing b-spline interpolation
oesteban Aug 13, 2018
4478c06
Add base implementation of transforms and change base implementation …
oesteban Mar 12, 2019
99fa15d
export to hdf5
oesteban Mar 13, 2019
5eed01d
corrections to adhere the current x5 format draft
oesteban Mar 14, 2019
632e068
fix coordinates translation in affines, import itk affines
oesteban Mar 15, 2019
f2c062e
wip: reads & writes ITK's MatrixOffsetTransformBase transforms, with …
oesteban Mar 16, 2019
8eb670d
fix: print warnings to stderr, sty: minimal fixes
oesteban Mar 16, 2019
799fb6e
enh(affines): write out AFNI 12-parameters files
oesteban Mar 19, 2019
ce36f06
enh(transforms): finish up a x5-to-fsl writer of affines
oesteban Mar 21, 2019
8e1bf44
enh(transforms): improve generation of x5 structures of reference spaces
oesteban Mar 21, 2019
e2df7cc
fix(transforms): string formating forgotten when writing ITK transforms
oesteban Mar 22, 2019
c5d10ed
wip(transforms): writing up some linear transform readers
oesteban Mar 22, 2019
a18ce1d
enh(transform): HDF5 - set values as attributes; generalize affines t…
oesteban Mar 22, 2019
9902f50
enh: apply some early comments from @effigies.
oesteban Sep 19, 2019
fe74efb
fix: add a first battery of tests
oesteban Sep 26, 2019
596994c
enh: add tests for affines stored in AFNI format (non-oblique images)
oesteban Sep 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add base implementation of transforms and change base implementation …
…of linear and nonlinear transforms
  • Loading branch information
oesteban committed Sep 27, 2019
commit 4478c062a336fd58b35e7d517bcfc8eef8d65291
140 changes: 10 additions & 130 deletions nibabel/transform/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

class ImageSpace(object):
'''Class to represent spaces of gridded data (images)'''
__slots__ = ['_affine', '_shape', '_ndim', '_ndindex', '_coords', '_nvox']
__slots__ = ['_affine', '_shape', '_ndim', '_ndindex', '_coords', '_nvox',
'_inverse']

def __init__(self, image):
self._affine = image.affine
Expand All @@ -23,13 +24,18 @@ def __init__(self, image):
self._nvox = np.prod(image.shape) # Do not access data array
self._ndindex = None
self._coords = None
self._inverse = np.linalg.inv(image.affine)
if self._ndim not in [2, 3]:
raise ValueError('Invalid image space (%d-D)' % self._ndim)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realigning 4D series seems like a pretty common use case that you're precluding here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think re-reading this, it's actually just that the reference image has to be 2 or 3D. Still, there's no reason I couldn't pass in a 4D image; it's just all in the same 3D space.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'll work on generalization later down the line.


@property
def affine(self):
return self._affine

@property
def inverse(self):
return self._inverse

@property
def shape(self):
return self._shape
Expand Down Expand Up @@ -166,133 +172,7 @@ def map_voxel(self, index, moving=None):
input reference voxel'''
raise NotImplementedError


class Affine(TransformBase):
'''Represents linear transforms on image data'''
__slots__ = ['_matrix']

def __init__(self, matrix):
'''Initialize a transform

Parameters
----------

matrix : ndarray
The inverse coordinate transformation matrix **in physical
coordinates**, mapping coordinates from *reference* space
into *moving* space.
This matrix should be provided in homogeneous coordinates.

Examples
--------

>>> xfm = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
>>> xfm.matrix # doctest: +NORMALIZE_WHITESPACE
array([[1, 0, 0, 4],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]])

def to_filename(self, filename):
'''Store the transform in BIDS-Transforms HDF5 file format (.x5).
'''
self._matrix = np.array(matrix)
assert self._matrix.ndim == 2, 'affine matrix should be 2D'
assert self._matrix.shape[0] == self._matrix.shape[1], 'affine matrix is not square'
super(Affine, self).__init__()

@property
def matrix(self):
return self._matrix

def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
output_ 8000 dtype=None):
'''Resample the moving image in reference space

Parameters
----------

moving : `spatialimage`
The image object containing the data to be resampled in reference
space
order : int, optional
The order of the spline interpolation, default is 3.
The order has to be in the range 0-5.
mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
Determines how the input image is extended when the resamplings overflows
a border. Default is 'constant'.
cval : float, optional
Constant value for ``mode='constant'``. Default is 0.0.
prefilter: bool, optional
Determines if the moving image's data array is prefiltered with
a spline filter before interpolation. The default is ``True``,
which will create a temporary *float64* array of filtered values
if *order > 1*. If setting this to ``False``, the output will be
slightly blurred if *order > 1*, unless the input is prefiltered,
i.e. it is the result of calling the spline filter on the original
input.

Returns
-------

moved_image : `spatialimage`
The moving imaged after resampling to reference space.


Examples
--------

>>> import nibabel as nib
>>> xfm = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
>>> ref = nib.load('image.nii.gz')
>>> xfm.reference = ref
>>> xfm.resample(ref, order=0)

'''
if output_dtype is None:
output_dtype = moving.header.get_data_dtype()

try:
reference = self.reference
except ValueError:
print('Warning: no reference space defined, using moving as reference')
reference = moving

# Compose an index to index affine matrix
matrix = reference.affine.dot(self._matrix.dot(np.linalg.inv(moving.affine)))
mdata = moving.get_data()
moved = ndi.affine_transform(
mdata, matrix=matrix[:mdata.ndim, :mdata.ndim],
offset=matrix[:mdata.ndim, mdata.ndim],
output_shape=reference.shape, order=order, mode=mode,
cval=cval, prefilter=prefilter)

moved_image = moving.__class__(moved, reference.affine, moving.header)
moved_image.header.set_data_dtype(output_dtype)

return moved_image

def map_point(self, coords, forward=True):
coords = np.array(coords)
if coords.shape[0] == self._matrix.shape[0] - 1:
coords = np.append(coords, [1])
affine = self._matrix if forward else np.linalg.inv(self._matrix)
return affine.dot(coords)[:-1]

def map_voxel(self, index, moving=None):
try:
reference = self.reference
except ValueError:
print('Warning: no reference space defined, using moving as reference')
reference = moving
else:
if moving is None:
moving = reference
finally:
if reference is None:
raise ValueError('Reference and moving spaces are both undefined')

index = np.array(index)
if index.shape[0] == self._matrix.shape[0] - 1:
index = np.append(index, [1])

matrix = reference.affine.dot(self._matrix.dot(np.linalg.inv(moving.affine)))
return tuple(matrix.dot(index)[:-1])
raise NotImplementedError
151 changes: 151 additions & 0 deletions nibabel/transform/linear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
# See COPYING file distributed along with the NiBabel package for the
# copyright and license terms.
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
''' Linear transforms '''
from __future__ import division, print_function, absolute_import
import numpy as np
from scipy import ndimage as ndi

from .base import ImageSpace, TransformBase
from ..funcs import four_to_three


class Affine(TransformBase):
'''Represents linear transforms on image data'''
__slots__ = ['_matrix']

def __init__(self, matrix):
'''Initialize a transform

Parameters
----------

matrix : ndarray
The inverse coordinate transformation matrix **in physical
coordinates**, mapping coordinates from *reference* space
into *moving* space.
This matrix should be provided in homogeneous coordinates.

Examples
--------

>>> xfm = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
>>> xfm.matrix # doctest: +NORMALIZE_WHITESPACE
array([[1, 0, 0, 4],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]])

'''
self._matrix = np.array(matrix)
assert self._matrix.ndim == 2, 'affine matrix should be 2D'
assert self._matrix.shape[0] == self._matrix.shape[1], 'affine matrix is not square'
super(Affine, self).__init__()

@property
def matrix(self):
return self._matrix

def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
output_dtype=None):
'''Resample the moving image in reference space

Parameters
----------

moving : `spatialimage`
The image object containing the data to be resampled in reference
space
order : int, optional
The order of the spline interpolation, default is 3.
The order has to be in the range 0-5.
mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
Determines how the input image is extended when the resamplings overflows
a border. Default is 'constant'.
cval : float, optional
Constant value for ``mode='constant'``. Default is 0.0.
prefilter: bool, optional
Determines if the moving image's data array is prefiltered with
a spline filter before interpolation. The default is ``True``,
which will create a temporary *float64* array of filtered values
if *order > 1*. If setting this to ``False``, the output will be
slightly blurred if *order > 1*, unless the input is prefiltered,
i.e. it is the result of calling the spline filter on the original
input.

Returns
-------

moved_image : `spatialimage`
The moving imaged after resampling to reference space.


Examples
--------

>>> import nibabel as nib
>>> xfm = Affine([[1, 0, 0, 4], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
>>> ref = nib.load('image.nii.gz')
>>> xfm.reference = ref
>>> xfm.resample(ref, order=0)

'''
if output_dtype is None:
output_dtype = moving.header.get_data_dtype()

try:
reference = self.reference
except ValueError:
print('Warning: no reference space defined, using moving as reference')
reference = moving

# Compose an index to index affine matrix
matrix = reference.affine.dot(self._matrix.dot(np.linalg.inv(moving.affine)))
mdata = moving.get_data()
moved = ndi.affine_transform(
mdata, matrix=matrix[:mdata.ndim, :mdata.ndim],
offset=matrix[:mdata.ndim, mdata.ndim],
output_sha 8000 pe=reference.shape, order=order, mode=mode,
cval=cval, prefilter=prefilter)

moved_image = moving.__class__(moved, reference.affine, moving.header)
moved_image.header.set_data_dtype(output_dtype)

return moved_image

def map_point(self, coords, forward=True):
coords = np.array(coords)
if coords.shape[0] == self._matrix.shape[0] - 1:
coords = np.append(coords, [1])
affine = self._matrix if forward else np.linalg.inv(self._matrix)
return affine.dot(coords)[:-1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this [:-1] be conditional on coords being appended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pushing this to the backlog checks that need be done before merge. For now I'm just working on a very much needed rebase and implementing all the functionalities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I've just realized why this [:-1] is here - to drop the last element of the homogeneous coordinate, which is always a one.


def map_voxel(self, index, moving=None):
try:
reference = self.reference
except ValueError:
print('Warning: no reference space defined, using moving as reference')
reference = moving
else:
if moving is None:
moving = reference
finally:
if reference is None:
raise ValueError('Reference and moving spaces are both undefined')

index = np.array(index)
if index.shape[0] == self._matrix.shape[0] - 1:
index = np.append(index, [1])

matrix = reference.affine.dot(self._matrix.dot(np.linalg.inv(moving.affine)))
return tuple(matrix.dot(index)[:-1])

def to_filename(self, filename):
'''Store the transform in BIDS-Transforms HDF5 file format (.x5).
'''
raise NotImplementedError
Loading
0