diff --git a/fsl/transform/__init__.py b/fsl/transform/__init__.py index 52fae19d69fc8ef0a04cfb1d3d4a4a74ce7d3238..8a93bf59438d427e3032598fd7c4b2db74d28ec1 100644 --- a/fsl/transform/__init__.py +++ b/fsl/transform/__init__.py @@ -34,7 +34,14 @@ from .flirt import ( # noqa flirtMatrixToSform, sformToFlirtMatrix) +from .fnirt import ( # noqa + readFnirt, + writeFnirt, + toFnirt, + fromFnirt) + from .x5 import ( # noqa - readFlirtX5, - writeFlirtX5 -) + readLinearX5, + writeLinearX5, + readNonLinearX5, + writeNonLinearX5) diff --git a/fsl/transform/flirt.py b/fsl/transform/flirt.py index 56b193e078b6e174725e46b0808a52c7e7f7b06c..97be2254a8dd2ef2ac72d7793090b87448993c9d 100644 --- a/fsl/transform/flirt.py +++ b/fsl/transform/flirt.py @@ -10,6 +10,8 @@ matrices. The following functions are available: .. autosummary:: :nosignatures: + readFlirt + writeFlirt fromFlirt toFlirt flirtMatrixToSform diff --git a/fsl/transform/fnirt.py b/fsl/transform/fnirt.py new file mode 100644 index 0000000000000000000000000000000000000000..d64e0589d8a193776277293f572cfb7e3d70cdee --- /dev/null +++ b/fsl/transform/fnirt.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# +# fnirt.py - Functions for working with FNIRT non-linear transformations. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module contains functions for working with FNIRT non-linear +transformation matrices. The following functions are available: + +.. autosummary:: + :nosignatures: + + readFnirt + writeFnirt +""" + + +import logging + +import fsl.data.constants as constants + + +log = logging.getLogger(__name__) + + +def readFnirt(fname, src, ref, dispType=None): + """ + """ + + # Figure out whether the file + # is a displacement field or + # a coefficient field + import fsl.data.image as fslimage + from . import nonlinear + + img = fslimage.Image(fname, loadData=False) + + dispfields = (constants.FSL_FNIRT_DISPLACEMENT_FIELD, + constants.FSL_TOPUP_FIELD) + coeffields = (constants.FSL_CUBIC_SPLINE_COEFFICIENTS, + constants.FSL_DCT_COEFFICIENTS, + constants.FSL_QUADRATIC_SPLINE_COEFFICIENTS, + constants.FSL_TOPUP_CUBIC_SPLINE_COEFFICIENTS, + constants.FSL_TOPUP_QUADRATIC_SPLINE_COEFFICIENTS) + + kwargs = { + 'src' : src, + 'ref' : ref, + 'srcSpace' : 'fsl', + 'refSpace' : 'fsl', + 'dispType' : None, + } + + if img.intent in dispfields: + return nonlinear.DisplacementField(fname, **kwargs) + + elif img.intent in coeffields: + pass # return nonlinear.CoefficientField(fname, **kwargs) + + else: + raise ValueError('Cannot determine type of nonlinear ' + 'file {}'.format(fname)) + + +def writeFnirt(field, fname): + """ + """ + field.save(fname) + + +def toFnirt(field): + pass + + +def fromFnirt(field, from_='voxel', to='world'): + """ + """ + + from . import nonlinear + + return nonlinear.convertDisplacementSpace(field, from_=from_, to=to) diff --git a/fsl/transform/nonlinear.py b/fsl/transform/nonlinear.py index 4cd0da681187631090607d70f2b6c64da266b4f6..7ddcf36dd5ba79073908eb04e9828341a515b1c4 100644 --- a/fsl/transform/nonlinear.py +++ b/fsl/transform/nonlinear.py @@ -4,6 +4,9 @@ # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module contains data structures and functions for working with +nonlinear transformations. +""" import numpy as np @@ -14,61 +17,124 @@ from . import affine class NonLinearTransform(fslimage.Image): - """Class which represents a FNIRT non-linear transformation + """Class which represents a nonlinear transformation. This is just a base + class for the :class:`DisplacementField` and :class:`CoefficientField` + classes. + + + A nonlinear transformation is an :class:`.Image` which contains + some mapping from a source image coordinate system to a reference image + coordinate system. """ - def __init__(self, *args, **kwargs): - """ + + def __init__(self, + image, + src, + ref=None, + srcSpace=None, + refSpace=None, + **kwargs): + """Create a ``NonLinearTransform``. + + :arg image: A string containing the name of an image file to load, + or a :mod:`numpy` array, or a :mod:`nibabel` image + object. + + :arg src: :class:`.Nifti` representing the sourceimage + + :arg ref: :class:`.Nifti` representing the reference image. + If not provided, it is assumed that this + ``NonLinearTransform`` is defined in the same + space as the reference. + + :arg srcSpace: Coordinate system in the source image that this + ``NonLinearTransform`` maps from. Defaults to ``'fsl'``. + + :arg refSpace: Coordinate system in the reference image that this + ``NonLinearTransform`` maps to. Defaults to ``'fsl'``. + + All other arguments are passed through to :meth:`.Image.__init__`. """ - src = kwargs.pop('src', None) - ref = kwargs.pop('ref', None) - srcSpace = kwargs.pop('srceSpace', 'fsl') - refSpace = kwargs.pop('refSpace', 'fsl') - fslimage.Image.__init__(self, *args, **kwargs) + if ref is None: ref = self + if srcSpace is None: srcSpace = 'fsl' + if refSpace is None: refSpace = 'fsl' + + if not (isinstance(src, (fslimage.Nifti, type(None))) and + isinstance(ref, fslimage.Nifti)): + raise ValueError('Invalid source/reference: {} -> {}'.format( + src, ref)) - if src is not None: src = src .header.copy() - if ref is not None: ref = ref .header.copy() - else: ref = self.header.copy() + if srcSpace not in ('fsl', 'voxel', 'world') or \ + refSpace not in ('fsl', 'voxel', 'world'): + raise ValueError('Invalid source/reference space: {} -> {}'.format( + srcSpace, refSpace)) - self.__src = src - self.__ref = ref + fslimage.Image.__init__(self, image, **kwargs) + + self.__src = fslimage.Nifti(src.header.copy()) + self.__ref = fslimage.Nifti(ref.header.copy()) self.__srcSpace = srcSpace self.__refSpace = refSpace @property def src(self): + """Return a reference to the :class:`.Nifti` instance representing + the source image. + """ return self.__src @property def ref(self): + """Return a reference to the :class:`.Nifti` instance representing + the reference image. + """ return self.__ref @property def srcSpace(self): + """Return the source image coordinate system this + ``NonLinearTransform`` maps from - see :meth:`.Nifti.getAffine`. + """ return self.__srcSpace @property def refSpace(self): + """Return the reference image coordinate system this + ``NonLinearTransform`` maps to - see :meth:`.Nifti.getAffine`. + """ return self.__refSpace class DisplacementField(NonLinearTransform): - """Class which represents a FNIRT displacement field which, at each voxel, + """Class which represents a displacement field which, at each voxel, contains an absolute or relative displacement from a source space to a reference space. """ + def __init__(self, *args, **kwargs): - """ + """Create a ``DisplacementField``. + + :arg dispType: Either ``'absolute'`` or ``'relative'``, indicating + the type of this displacement field. If not provided, + will be inferred via the :func:`detectDisplacementType` + function. + + All other arguments are passed through to + :meth:`NonLinearTransform.__init__`. """ dispType = kwargs.pop('dispType', None) + if dispType not in (None, 'relative', 'absolute'): + raise ValueError('Invalid value for dispType: {}'.format(dispType)) + NonLinearTransform.__init__(self, *args, **kwargs) self.__dispType = dispType @@ -76,6 +142,9 @@ class DisplacementField(NonLinearTransform): @property def displacementType(self): + """The type of this ``DisplacementField`` - ``'absolute'`` or + ``'relative'``. + """ if self.__dispType is None: self.__dispType = detectDisplacementType(self) return self.__dispType @@ -83,14 +152,23 @@ class DisplacementField(NonLinearTransform): @property def absolute(self): + """``True`` if this ``DisplacementField`` contains absolute + displacements. + """ return self.displacementType == 'absolute' @property def relative(self): + """``True`` if this ``DisplacementField`` contains relative + displacements. + """ return self.displacementType == 'relative' + def transform(self, coords): + raise NotImplementedError() + def detectDisplacementType(field): """Attempt to automatically determine whether a displacement field is @@ -104,9 +182,9 @@ def detectDisplacementType(field): # This test is based on the assumption # that a displacement field containing - # absolute oordinates will have a greater - # standard deviation than one which - # contains relative coordinates. + # absolute coordinates will have a + # greater standard deviation than one + # which contains relative coordinates. absdata = field[:] reldata = convertDisplacementType(field, 'relative') stdabs = absdata.std(axis=(0, 1, 2)).sum() @@ -119,6 +197,12 @@ def detectDisplacementType(field): def convertDisplacementType(field, dispType=None): """Convert a displacement field between storing absolute and relative displacements. + + :arg field: A :class:`DisplacementField` instance + :arg dispType: Either ``'absolute'`` or ``'relative'``. If not provided, + the opposite type to ``field.displacementType`` is used. + :returns: A ``numpy.array`` containing the adjusted displacement + field. """ if dispType is None: @@ -146,13 +230,19 @@ def convertDisplacementType(field, dispType=None): elif dispType == 'relative': return field.data - coords -def convertDisplacementSpace(field, src, from_, to, ref=None, dispType=None): +def convertDisplacementSpace(field, from_, to): """Adjust the source and/or reference spaces of the given displacement - field. - """ + field. See the :meth:`.Nifti.getAffine` method for the valid values for + the ``from_`` and ``to`` arguments. + + :arg field: A :class:`DisplacementField` instance + :arg from_: New source image coordinate system + :arg to: New reference image coordinate system - if ref is None: ref = field - if dispType is None: dispType = field.displacementType + :returns: A new :class:`DisplacementField` which transforms from + the source ``from_`` coordinate system to the reference ``to`` + coordinate system. + """ # Get the field in absolute # coordinates if necessary @@ -161,10 +251,10 @@ def convertDisplacementSpace(field, src, from_, to, ref=None, dispType=None): else: srccoords = fieldcoords # Now transform those source - # coordinates from the original + # coordinates from the original # source space to the source # space specified by "from_" - srcmat = src.getAffine(field.srcSpace, from_) + srcmat = field.src.getAffine(field.srcSpace, from_) srccoords = srccoords.reshape((-1, 3)) srccoords = affine.transform(srccoords, srcmat) @@ -172,7 +262,7 @@ def convertDisplacementSpace(field, src, from_, to, ref=None, dispType=None): # an absolute displacement, the # reference "to" coordinate system # is irrelevant - we're done. - if dispType == 'absolute': + if field.absolute: fieldcoords = srccoords # Otherwise our displacement field @@ -184,7 +274,7 @@ def convertDisplacementSpace(field, src, from_, to, ref=None, dispType=None): # displacements from source "from_" # space into reference "to" space. else: - refmat = ref.getAffine(field.refSpace, to) + refmat = field.ref.getAffine(field.refSpace, to) refcoords = fieldcoords.reshape((-1, 3)) refcoords = affine.transform(refcoords, refmat) fieldcoords = srccoords - refcoords @@ -192,8 +282,8 @@ def convertDisplacementSpace(field, src, from_, to, ref=None, dispType=None): return DisplacementField( fieldcoords.reshape(field.shape), header=field.header, - src=src, - ref=ref, + src=field.src, + ref=field.ref, srcSpace=from_, refSpace=to, - dispType=dispType) + dispType=field.displacementType) diff --git a/fsl/transform/x5.py b/fsl/transform/x5.py index 7f6f8c404b16c8e933afbce6829ef179f1b11793..d6659a5e7ee6916e68b5a737dd72f7e020ec0bb8 100644 --- a/fsl/transform/x5.py +++ b/fsl/transform/x5.py @@ -17,13 +17,13 @@ import numpy.linalg as npla import nibabel as nib import h5py -from . import flirt +import fsl.version as version -def _writeLinearTransform(group, xform): - group.attrs['Type'] = 'linear' - group.create_dataset('Transform', data=xform) - group.create_dataset('Inverse', data=npla.inv(xform)) +def _writeMetadata(group): + group.attrs['Format'] = 'X5' + group.attrs['Version'] = '0.0.1' + group.attrs['Metadata'] = json.dumps({'fslpy' : version.__version__}) def _readLinearTransform(group): @@ -32,13 +32,15 @@ def _readLinearTransform(group): return np.array(group['Transform']) -def _writeLinearMapping(group, img): - group.attrs['Type'] = 'image' - group.attrs['Size'] = img.shape[ :3] - group.attrs['Scales'] = img.pixdim[:3] +def _writeLinearTransform(group, xform): + + xform = np.asarray(xform, dtype=np.float32) + inv = np.asarray(npla.inv(xform), dtype=np.float32) + + group.attrs['Type'] = 'linear' + group.create_dataset('Transform', data=xform) + group.create_dataset('Inverse', data=inv) - mapping = group.create_group('Mapping') - _writeLinearTransform(mapping, img.getAffine('voxel', 'world')) def _readLinearMapping(group): @@ -58,17 +60,71 @@ def _readLinearMapping(group): return fslimage.Nifti(hdr) -def writeFlirtX5(fname, xform, src, ref): +def _writeLinearMapping(group, img): + group.attrs['Type'] = 'image' + group.attrs['Size'] = np.asarray(img.shape[ :3], np.uint32) + group.attrs['Scales'] = np.asarray(img.pixdim[:3], np.float32) + + mapping = group.create_group('Mapping') + _writeLinearTransform(mapping, img.getAffine('voxel', 'world')) + + +def _readNonLinearTransform(group): + if group.attrs['Type'] != 'nonlinear': + raise ValueError('Not a nonlinear transform') + return np.array(group['Transform']) + + +def _writeNonLinearTransform(group, field): """ """ + group.attrs['Type'] = 'nonlinear' + group.create_dataset('Transform', data=field, dtype=np.float32) - xform = flirt.fromFlirt(xform, src, ref, 'world', 'world') - with h5py.File(fname, 'w') as f: - f.attrs['Format'] = 'X5' - f.attrs['Version'] = '0.0.1' - f.attrs['Metadata'] = json.dumps({'software' : 'flirt'}) +def readLinearX5(fname): + """ + """ + with h5py.File(fname, 'r') as f: + xform = _readLinearTransform(f['/']) + src = _readLinearMapping( f['/From']) + ref = _readLinearMapping( f['/To']) + + return xform, src, ref + + +def writeLinearX5(fname, xform, src, ref): + """ + + :: + /Format # "X5" + /Version # "0.0.1" + /Metadata # json string containing unstructured metadata + + /Type # "linear" + /Transform # the transform itself + /Inverse # optional pre-calculated inverse + + /From/Type # "image" - could in principle be something other than + # "image" (e.g. "surface"), in which case the "Size" and + # "Scales" entries might be replaced with something else + /From/Size # voxel dimensions + /From/Scales # voxel pixdims + /From/Mapping/Type # "linear" - could be also be "nonlinear" + /From/Mapping/Transform # source voxel-to-world sform + /From/Mapping/Inverse # optional inverse + + /To/Type # "image" + /To/Size # voxel dimensions + /To/Scales # voxel pixdims + /To/Mapping/Type # "linear" + /To/Mapping/Transform # reference voxel-to-world sform + /To/Mapping/Inverse # optional inverse + """ + + with h5py.File(fname, 'w') as f: + _writeMetadata(f) _writeLinearTransform(f, xform) from_ = f.create_group('/From') @@ -78,12 +134,59 @@ def writeFlirtX5(fname, xform, src, ref): _writeLinearMapping(to, ref) -def readFlirtX5(fname): +def readNonLinearX5(fname): """ """ + + from . import nonlinear + with h5py.File(fname, 'r') as f: - xform = _readLinearTransform(f['/']) - src = _readLinearMapping( f['/From']) - ref = _readLinearMapping( f['/To']) + field = _readNonLinearTransform(f['/']) + src = _readLinearMapping(f['/From']) + ref = _readLinearMapping(f['/To']) - return xform, src, ref + # TODO coefficient fields + return nonlinear.DisplacementField(field, + src=src, + ref=ref, + srcSpace='world', + refSpace='world') + + +def writeNonLinearX5(fname, field): + """ + :: + /Format # "X5" + /Version # "0.0.1" + /Metadata # json string containing unstructured metadata + + /Type # "nonlinear" + /Transform # the displacement/coefficient field itself + /Inverse # optional pre-calculated inverse + + /From/Type # "image" + /From/Size # voxel dimensions + /From/Scales # voxel pixdims + /From/Mapping/Type # "linear" + /From/Mapping/Transform # source voxel-to-world sform + /From/Mapping/Inverse # optional inverse + + /To/Type # "image" + /To/Size # voxel dimensions + /To/Scales # voxel pixdims + /To/Mapping/Type # "linear" + /To/Mapping/Transform # reference voxel-to-world sform + /To/Mapping/Inverse # optional inverse + """ + + # TODO coefficient fields + + with h5py.File(fname, 'w') as f: + _writeMetadata(f) + _writeNonLinearTransform(f, field.data) + + from_ = f.create_group('/From') + to = f.create_group('/To') + + _writeLinearMapping(from_, field.src) + _writeLinearMapping(to, field.ref)