diff --git a/fsl/scripts/fsl_convert_x5.py b/fsl/scripts/fsl_convert_x5.py index 87fc13dc5c5f1f13d0b871ba7cd5116ce09b3272..5dc220cf021e8c967054cb465a6e9f733801815f 100644 --- a/fsl/scripts/fsl_convert_x5.py +++ b/fsl/scripts/fsl_convert_x5.py @@ -1,9 +1,12 @@ #!/usr/bin/env python # -# fsl_convert_x5.py - +# fsl_convert_x5.py - Convert between FSL and X5 transformation files. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This script can be used to convert between FSL and X5 linear and non-linear +transformation file formats. +""" import os.path as op @@ -78,12 +81,16 @@ def flirtToX5(args): def X5ToFlirt(args): + """Convert a linear X5 transformation file to a FLIRT matrix. """ xform, src, ref = transform.readLinearX5(args.input) xform = transform.toFlirt(xform, src, ref, 'world', 'world') transform.writeFlirt(xform, args.output) def fnirtToX5(args): + """Convert a non-linear FNIRT transformation into an X5 transformation + file. + """ src = fslimage.Image(args.source, loadData=False) ref = fslimage.Image(args.reference, loadData=False) field = transform.readFnirt(args.input, @@ -95,12 +102,16 @@ def fnirtToX5(args): def X5ToFnirt(args): + """Convert a non-linear X5 transformation file to a FNIRT-style + transformation file. + """ field = transform.readNonLinearX5(args.input) field = transform.toFnirt(field, 'world', 'world') transform.writeFnirt(field, args.output) def doFlirt(args): + """Converts a linear FIRT transformation file to or from the X5 format. """ infmt = args.input_format outfmt = args.output_format @@ -110,6 +121,9 @@ def doFlirt(args): def doFnirt(args): + """Converts a non-linear FNIRT transformation file to or from the X5 + format. + """ infmt = args.input_format outfmt = args.output_format @@ -119,15 +133,22 @@ def doFnirt(args): def main(args=None): + """Entry point. Calls :func:`doFlirt` or :func:`doFnirt` depending on + the sub-command specified in the arguments. + + :arg args: Sequence of command-line arguments. If not provided, + ``sys.argv`` is used. + """ if args is None: args = sys.argv[1:] - args = parseArgs(args) - ctype = args.ctype + args = parseArgs(args) + + if args.ctype == 'flirt': doFlirt(args) + elif args.ctype == 'fnirt': doFnirt(args) - if ctype == 'flirt': doFlirt(args) - elif ctype == 'fnirt': doFnirt(args) + return 0 if __name__ == '__main__': diff --git a/fsl/transform/x5.py b/fsl/transform/x5.py index 1ebd1daabf05344cdf20f7a088789f4fcfd7e546..b37dee2d75f6eeeec3049b2c9447cf7140d81e03 100644 --- a/fsl/transform/x5.py +++ b/fsl/transform/x5.py @@ -1,41 +1,241 @@ #!/usr/bin/env python # -# x5.py - +# x5.py - Functions for working with BIDS X5 files. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -""" +"""This module contains functions for reading/writing linear/non-linear FSL +transformations from/to BIDS X5 files. + + +An X5 file is a HDF5 container file which stores a linear or non-linear +transformation between a **source** NIfTI image and a **refernece** image. In +an X5 file, the source image is referred to as the ``From`` space, and the +reference image the ``To`` space. + + +Custom HDF5 groups +------------------ + + +HDF5 files are composed of *groups*, *attributes*, and *datasets*. Groups are +simply containers for attributes, datasets, and other groups. Attributes are +strongly-typed scalar values. Datasets are strongly-typed, structured +N-dimensional arrays. + + +To simplify the file format efinitions below, we shall first define a few +custom HDF5 groups. In the file format definitions, a HDF5 group which is +listed as being of one of these custom types shall contain the +attributes/datasets that are listed here. + + +*affine* +^^^^^^^^ + + +A HDF5 group which is listed as being of type "affine" contains an affine +transformation, which can be used to transform coordinates from one space into +another. Groups of type "affine" have the following fields: + + ++---------------+-----------+-------------------------------------------------+ +| Name | Type | Value/Description | ++---------------+-----------+-------------------------------------------------+ +| ``Type`` | attribute | ``'linear'`` | +| ``Transform`` | dataset | The transformation itself - a 4x4 ``float64`` | +| | | affine transformation | +| ``Inverse`` | dataset | Optional pre-calculated inverse | ++---------------+-----------+-------------------------------------------------+ + + +*mapping* +^^^^^^^^^ + + +A HDF5 group which is listed as being of type "mapping" contains all of the +information required to define the space of a NIfTI image, including its +dimensions, and voxel-to-world affine transformation. The *world* coordinate +system of an image is defined by its ``sform`` or ``qform`` (hereafter +referred to as the ``sform``), which is contained in the NIfTI header. + + +Groups of type "mapping" have the following fields: + + ++-------------+-----------+---------------------------------------------------+ +| Name | Type | Value/Description | ++-------------+-----------+---------------------------------------------------+ +| ``Type`` | attribute | ``'image'`` | +| ``Size`` | attribute | ``uint64`` ``(X, Y, Z)`` voxel dimensions | +| ``Scales`` | attribute | ``float64`` ``(X, Y, Z)`` voxel pixdims | +| ``Mapping`` | affine | The image voxel-to-world transformation (its | +| | | "sform") | ++-------------+-----------+---------------------------------------------------+ + + +Linear X5 files +--------------- + + +Linear X5 transformation files contain an affine transformation matrix of +shape ``(4, 4)``, which can be used to transform **source image world +coordinates into reference image world coordinates**. + + +Linear X5 transformation files are assumed to adhere to the HDF5 structure +defined in the table below. All fields are required. + + ++-----------------------------+-----------+-----------------------------------+ +| Name | Type | Value/Description | ++-----------------------------+-----------+-----------------------------------+ +| *Metadata* | ++-----------------------------+-----------+-----------------------------------+ +| ``/Format`` | attribute | ``'X5'`` | +| ``/Version`` | attribute | ``'0.0.1'`` | +| ``/Metadata`` | attribute | JSON string containing | +| | | unstructured metadata. | ++-----------------------------+-----------+-----------------------------------+ +| *Transformation* | ++-----------------------------+-----------+-----------------------------------+ +| ``/From/`` | mapping | Source image mapping | +| ``/To/`` | mapping | Reference image mapping | +| ``/`` | affine | Affine transformation from source | +| | | image world coordinates to | +| | | reference image world coordinates | ++-----------------------------+-----------+-----------------------------------+ + + +Non-linear X5 transformation files +---------------------------------- + + +Non-linear X5 transformation files contain a non-linear transformation between +a source image coordinate system and a reference image coordinate system. The +transformation is represented as either: + + - A *displacement field*, which is defined in the same space as the reference + image, and which contains displacements. + + - A quadratic or cubic B-spline *coefficient field*, which contains + coefficients defined in a coarse grid which overlays the same space as the + reference image, and from which a displacement field can be calculated. + +Displacement fields may contain either: + + - *relative displacements*, which contains displacements *from* the + reference image coordinates *to* the source image coordinates (i.e. add the + displacements to the reference image coordinates to get the corresponding + source image coordinates) + + - *absolute displacement* which simply contain the source image coordinates + at each voxel in the reference image space. + + +Displacement field transformations define displacements between the refernece +image *world* coordinate system, and the source image *world* coordinate +system. + + +The displacement field generated from a coefficient field defines relative +displacements from a reference image space to a source image space. These +spaces are undefined; however, the ``/Pre`` affine transformation may be used +to transform reference image world coordinates into the reference image space, +and the ``/Post`` affine transformation may be used to transform the resulting +source image coordinates into the source image world coordinate system. + + +Non-linear X5 transformation files are assumed to adhere to the following +HDF5 structure:: + + ++-----------------------------+-----------+-----------------------------------+ +| Name | Type | Value/Description | ++-----------------------------+-----------+-----------------------------------+ +| *Metadata* - same as for linear transformation files | ++-----------------------------------------------------------------------------+ +| *Transformation* | ++-----------------------------+-----------+-----------------------------------+ +| ``/Type`` | attribute | ``'nonlinear'`` | +| ``/SubType`` | attribute | ``'displacement'`` or | +| | | ``'coefficient'`` | +| ``/Representation`` | attribute | If ``/SubType`` is | +| | | ``'displacement'``, its | +| | | ``/Representation`` may be either | +| | | ``'absolute'`` or ``'relative'``. | +| | | If ``/SubType`` is | +| | | ``'coefficient'``, its | +| | | ``/Representation`` may be either | +| | | ``'quadratic bspline'`` or | +| | | ``'cubic bspline'``. | +| ``/Transform`` | dataset | The transformation itself - see | +| | | above. | +| ``/Inverse`` | dataset | Optional pre-calculated inverse | +| ``/From/`` | mapping | Source image mapping | +| ``/To/`` | mapping | Reference image mapping | +|-----------------------------+-----------+-----------------------------------+ +| *Coefficient field parameters* (required for ``'coefficient'`` files) | +|-----------------------------+-----------+-----------------------------------+ +| ``/Parameters/Spacing`` | attribute | +| ``/Parameters/ReferenceToField`` | affine | | attribute | ++-----------------------------+-----------+-----------------------------------+ -Functions for reading/writing linear/non-linear FSL transformations from/to -BIDS X5 files. """ + import json -import numpy as np -import numpy.linalg as npla -import nibabel as nib +import numpy as np +import nibabel as nib import h5py import fsl.version as version +from . import affine + + +X5_FORMAT = 'X5' +X5_VERSION = '0.0.1' def _writeMetadata(group): - group.attrs['Format'] = 'X5' - group.attrs['Version'] = '0.0.1' + """Writes a metadata block into the given group. + + :arg group: A ``h5py.Group`` object. + """ + group.attrs['Format'] = X5_FORMAT + group.attrs['Version'] = X5_VERSION group.attrs['Metadata'] = json.dumps({'fslpy' : version.__version__}) def _readLinearTransform(group): + """Reads a linear transformation from the given group, + + :arg group: A ``h5py.Group`` object. + :returns: ``numpy`` array containing a ``(4, 4)`` affine transformation. + """ + if group.attrs['Type'] != 'linear': raise ValueError('Not a linear transform') - return np.array(group['Transform']) + + xform = np.array(group['Transform']) + + if xform.shape != (4, 4): + raise ValueError('Not a linear transform') + + return xform def _writeLinearTransform(group, xform): + """Writes the given linear transformation and its inverse to the given + group. + + :arg group: A ``h5py.Group`` object. + :arg xform: ``numpy`` array containing a ``(4, 4)`` affine transformation. + """ - xform = np.asarray(xform, dtype=np.float32) - inv = np.asarray(npla.inv(xform), dtype=np.float32) + xform = np.asarray(xform, dtype=np.float64) + inv = np.asarray(affine.inverse(xform), dtype=np.float64) group.attrs['Type'] = 'linear' group.create_dataset('Transform', data=xform) @@ -43,6 +243,12 @@ def _writeLinearTransform(group, xform): def _readLinearMapping(group): + """Reads a linear mapping, defining a source or reference space, from the + given group. + + :arg group: A ``h5py.Group`` object. + :returns: :class:`.Nifti` object. defining the mapping + """ import fsl.data.image as fslimage @@ -61,6 +267,11 @@ def _readLinearMapping(group): def _writeLinearMapping(group, img): + """Writes a linear mapping specified by ``img`` to the given group. + + :arg group: A ``h5py.Group`` object. + :arg img: :class:`.Nifti` object. defining the mapping + """ group.attrs['Type'] = 'image' group.attrs['Size'] = np.asarray(img.shape[ :3], np.uint32) group.attrs['Scales'] = np.asarray(img.pixdim[:3], np.float32) @@ -95,33 +306,7 @@ def readLinearX5(fname): 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" - /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 - """ # noqa + """ with h5py.File(fname, 'w') as f: _writeMetadata(f)