From b0271a872a9a65332a5f4bc20b4ed181d8a32398 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Tue, 1 Sep 2015 15:49:42 +0100 Subject: [PATCH] Documentation for all modules in the fsl.data package. --- doc/fsl.data.rst | 16 -- fsl/data/__init__.py | 16 +- fsl/data/atlases.py | 246 ++++++++++++++++++++------- fsl/data/constants.py | 76 +++++++-- fsl/data/featimage.py | 103 +++++++++-- fsl/data/featresults.py | 143 +++++++++++++--- fsl/data/image.py | 190 ++++++++++++--------- fsl/data/model.py | 136 ++++++++++----- fsl/data/strings.py | 23 ++- fsl/fsleyes/views/timeseriespanel.py | 2 +- fsl/utils/colourbarbitmap.py | 2 +- 11 files changed, 710 insertions(+), 243 deletions(-) diff --git a/doc/fsl.data.rst b/doc/fsl.data.rst index f768559d5..31516eabd 100644 --- a/doc/fsl.data.rst +++ b/doc/fsl.data.rst @@ -1,22 +1,6 @@ fsl.data package ================ -Submodules ----------- - -.. toctree:: - - fsl.data.atlases - fsl.data.constants - fsl.data.featimage - fsl.data.featresults - fsl.data.image - fsl.data.model - fsl.data.strings - -Module contents ---------------- - .. automodule:: fsl.data :members: :undoc-members: diff --git a/fsl/data/__init__.py b/fsl/data/__init__.py index 27a9696ea..7372461c5 100644 --- a/fsl/data/__init__.py +++ b/fsl/data/__init__.py @@ -4,4 +4,18 @@ # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -"""Data structures and models.""" +"""This module contains various data types and associated I/O routines, +models, constants, and other data-like things used throughout ``fslpy``. + + + .. autosummary:: + :nosignatures: + + ~fsl.data.image.Image + ~fsl.data.featimage.FEATImage + ~fsl.data.model.Model + ~fsl.data.featresults + ~fsl.data.atlases + ~fsl.data.strings + ~fsl.data.constants +""" diff --git a/fsl/data/atlases.py b/fsl/data/atlases.py index dd5165a7b..b747ae867 100644 --- a/fsl/data/atlases.py +++ b/fsl/data/atlases.py @@ -6,45 +6,82 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """This module provides access to the atlas images which are contained in -``$FSLDIR/data/atlases/``. - -Instances of the :class:`Atlas` class is a - -MNI152 - - -<atlas> - <header> - <name></name> # Atlas name - <type></type> # 'Probabilistic' or 'Label' - <images> - <imagefile> - </imagefile> # If type is Probabilistic, path - # to 4D image file, one volume per - # label, Otherwise, if type is - # Label, path to 3D label file - # (identical to the summaryimagefile - # below) - - <summaryimagefile> # Path to 3D summary file, with each - </summaryimagefile> # region having value (index + 1) - - </images> - ... # More images - generally both - # 1mm and 2mm versions (in - # MNI152 space) are available - </header> - <data> - # index - index of corresponding volume in 4D image file - # x | - # y |- XYZ *voxel* coordinates into the first image of the <images> list - # z | - <label index="0" x="0" y="0" z="0">Name</label> - ... - </data> -</atlas> +``$FSLDIR/data/atlases/``. This directory contains XML files which describe +all of the available atlases. An XML atlas description file is assumed to +have a structure that looks like the following: + +.. code-block:: xml + + <atlas> + <header> + <name></name> # Atlas name + <type></type> # 'Probabilistic' or 'Label' + <images> + <imagefile> + </imagefile> # If type is Probabilistic, path + # to 4D image file, one volume per + # label, Otherwise, if type is + # Label, path to 3D label file + # (identical to the summaryimagefile + # below) + + <summaryimagefile> # Path to 3D summary file, with each + </summaryimagefile> # region having value (index + 1) + + </images> + ... # More images - generally both + # 1mm and 2mm versions (in + # MNI152 space) are available + </header> + <data> + + # index - For probabilistic atlases, index of corresponding volume in + # 4D image file. For label images, the value of voxels which + # are in the corresponding region. + # + # x | + # y |- XYZ *voxel* coordinates into the first image of the <images> + # | list + # z | + <label index="0" x="0" y="0" z="0">Name</label> + ... + </data> + </atlas> + + +This module reads in all of these XML files, and builds a list of +:class:`AtlasDescription` instances, each of which contains information about +one atlas. Each atlas is assigned an identifier, which is simply the XML file +name describing the atlas, sans-suffix. For exmaple, the atlas described by: + + ``$FSLDIR/data/atlases/HarvardOxford-Cortical.xml`` + +is given the identifier + + ``HarvardOxford-Cortical`` + + +The following functions provide access to the available +:class:`AtlasDescription` instances: + +.. autosummary:: + :nosignatures: + + listAtlases + getAtlasDescription + + +The :func:`loadAtlas` function allows you to load an atlas image, which will +be one of the following atlas-specific :class:`.Image` sub-classes: + +.. autosummary:: + :nosignatures: + + LabelAtlas + ProbabilisticAtlas """ + import os import xml.etree.ElementTree as et import os.path as op @@ -61,30 +98,14 @@ import fsl.utils.transform as transform log = logging.getLogger(__name__) -ATLAS_DIR = None - -def _setAtlasDir(): - global ATLAS_DIR - - if ATLAS_DIR is not None: - return - - if os.environ.get('FSLDIR', None) is None: - log.warn('$FSLDIR is not set - atlases are not available') - else: - ATLAS_DIR = op.join(os.environ['FSLDIR'], 'data', 'atlases') - -ATLAS_DESCRIPTIONS = collections.OrderedDict() - - def listAtlases(refresh=False): """Returns a dictionary containing :class:`AtlasDescription` objects for all available atlases. :arg refresh: If ``True``, or if the atlas desriptions have not previously been loaded, atlas descriptions are - loaded from the atlas files. Otherwise, prefviously + loaded from the atlas files. Otherwise, previously loaded descriptions are returned (see :attr:`ATLAS_DESCRIPTIONS`). """ @@ -109,7 +130,6 @@ def listAtlases(refresh=False): for i, desc in enumerate(atlasDescs): desc.index = i ATLAS_DESCRIPTIONS[desc.atlasID] = desc - return atlasDescs @@ -161,12 +181,50 @@ def loadAtlas(atlasID, loadSummary=False): class AtlasDescription(object): - """Loads the data stored in an Atlas XML description, and makes said - information accessible via instance attributes. + """An ``AtlasDescription`` instance parses and stores the information + stored in the XML file that describes one atlas. + + The following attributes are available on an ``AtlasDescription`` instance: + + ================= ====================================================== + ``atlasID`` The atlas ID, as described above. + ``name`` Name of the atlas. + ``atlasType`` Atlas type - either *probabilistic* or *label*. + ``images`` A list of images available for this atlas - usually + :math:`1mm^3` and :math:`2mm^3` images are present. + ``summaryImages`` For probabilistic atlases, a list of *summary* images, + which are just 3D labelled variants of the atlas. + ``labels`` A list of ``AtlasLabel`` objects, describing each + region / label in the atlas. + ================= ====================================================== + + Each ``AtlasLabel`` instance in the ``labels`` list contains the + following attributes: + + ========= ============================================================== + ``name`` Region name + ``index`` For probabilistic atlases, the volume index into the 4D atlas + image that corresponds to this region. For label atlases, the + value of voxels that are in this region. For summary images of + probabilistic atlases, add 1 to this value to get the + corresponding voxel values. + ``x`` X coordinate of the region in world space + ``y`` Y coordinate of the region in world space + ``z`` Z coordinate of the region in world space + ========= ============================================================== + + .. note:: The ``x``, ``y`` and ``z`` label coordinates are pre-calculated + centre-of-gravity coordinates, as listed in the atlas xml file. + They are in the coordinate system defined by the atlas image + transformation matrix (typically MNI152 space). """ def __init__(self, filename): + """Create an ``AtlasDescription`` instance. + + :arg filename: Name of the XML file describing the atlas. + """ log.debug('Loading atlas description from {}'.format(filename)) @@ -237,8 +295,21 @@ class AtlasDescription(object): class Atlas(fslimage.Image): + """This is the base class for the :class:`LabelAtlas` and + :class:`ProbabilisticAtlas` classes. It contains some initialisation + logic common to both. + """ + def __init__(self, atlasDesc, isLabel=False): + """Initialise an ``Atlas``. + + :arg atlasDesc: The :class:`AtlasDescription` instance which describes + the atlas. + + :arg isLabel: Pass in ``True`` for label atlases, ``False`` for + probabilistic atlases. + """ # Choose the atlas image # with the highest resolution @@ -262,17 +333,30 @@ class Atlas(fslimage.Image): # their sform_codes are correctly set self.nibImage.get_header().set_sform( None, code=constants.NIFTI_XFORM_MNI_152) - self.desc = atlasDesc class LabelAtlas(Atlas): + """A 3D atlas which contains integer labels for each region. + + The ``LabelAtlas`` class provides the :meth:`label` method, which + makes looking up the label at a location easy. + """ def __init__(self, atlasDesc): + """Create a ``LabelAtlas`` instance. + + :arg atlasDesc: The :class:`AtlasDescription` instance describing + the atlas. + """ Atlas.__init__(self, atlasDesc, isLabel=True) + def label(self, worldLoc): + """Looks up and returns the label of the region at the given world + location, or ``np.nan`` if the location is out of bounds. + """ voxelLoc = transform.transform([worldLoc], self.worldToVoxMat.T)[0] @@ -294,12 +378,27 @@ class LabelAtlas(Atlas): class ProbabilisticAtlas(Atlas): + """A 4D atlas which contains one volume for each region. + + The ``ProbabilisticAtlas`` provides the :meth`proportions` method, + which makes looking up region probabilities easy. + """ def __init__(self, atlasDesc): + """Create a ``ProbabilisticAtlas`` instance. + + :arg atlasDesc: The :class:`AtlasDescription` instance describing + the atlas. + """ Atlas.__init__(self, atlasDesc, isLabel=False) def proportions(self, worldLoc): + """:returns: a list of values, one per region, which represent + the probability of each region for the given world + location. Returns an empty list if the given + location is out of bounds. + """ voxelLoc = transform.transform([worldLoc], self.worldToVoxMat.T)[0] if voxelLoc[0] < 0 or \ @@ -311,3 +410,34 @@ class ProbabilisticAtlas(Atlas): return [] return self.data[voxelLoc[0], voxelLoc[1], voxelLoc[2], :] + + + +ATLAS_DIR = None +"""This attribute stores the absolute path to ``$FSLDIR/data/atlases/``. It is +``None`` if ``$FSLDIR`` is not set. See :func:`_setAtlasDir`. +""" + + +ATLAS_DESCRIPTIONS = collections.OrderedDict() +"""This dictionary contains an ``{atlasID : AtlasDescription}`` mapping for +all atlases contained in ``$FSLDIR/data/atlases/``. +""" + + +def _setAtlasDir(): + """Called by the :func:`listAtlases`, :func:`getAtlasDescription` and + :func:`loadAtlas` functions. + + Sets the :data:`ATLAS_DIR` attribute if it has not already been set, and + if the ``$FSLDIR`` environment variable is set. + """ + global ATLAS_DIR + + if ATLAS_DIR is not None: + return + + if os.environ.get('FSLDIR', None) is None: + log.warn('$FSLDIR is not set - atlases are not available') + else: + ATLAS_DIR = op.join(os.environ['FSLDIR'], 'data', 'atlases') diff --git a/fsl/data/constants.py b/fsl/data/constants.py index d2fa0bfde..52a2f8200 100644 --- a/fsl/data/constants.py +++ b/fsl/data/constants.py @@ -4,22 +4,78 @@ # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module defines some constant values used throughout ``fsleyes``. + + +The following constants relate to the orientation of an axis, in either +voxel or world space: + +.. autosummary:: + ORIENT_L2R + ORIENT_R2L + ORIENT_P2A + ORIENT_A2P + ORIENT_I2S + ORIENT_S2I + ORIENT_UNKNOWN + + +These constants relate to the *space* in which a NIFTI1 image is assumed to be +(i.e. the transformed coordinate space); they are defined in the NIFTI1 +specification: + +.. autosummary:: + NIFTI_XFORM_UNKNOWN + NIFTI_XFORM_SCANNER_ANAT + NIFTI_XFORM_ALIGNED_ANAT + NIFTI_XFORM_TALAIRACH + NIFTI_XFORM_MNI_152 +""" + + +ORIENT_L2R = 0 +"""The axis goes from left to right.""" + + +ORIENT_R2L = 1 +"""The axis goes from right to left.""" + + +ORIENT_P2A = 2 +"""The axis goes from posterior to anterior.""" + + +ORIENT_A2P = 3 +"""The axis goes from anterior to posterior.""" + + +ORIENT_I2S = 4 +"""The axis goes from inferior to superior.""" + + +ORIENT_S2I = 5 +"""The axis goes from superior to inferior.""" + -# Constants which represent the orientation -# of an axis, in either voxel or world space. ORIENT_UNKNOWN = -1 -ORIENT_L2R = 0 -ORIENT_R2L = 1 -ORIENT_P2A = 2 -ORIENT_A2P = 3 -ORIENT_I2S = 4 -ORIENT_S2I = 5 +"""The axis has an unknown orientation.""" -# Constants from the NIFTI1 specification that define -# the 'space' in which an image is assumed to be. NIFTI_XFORM_UNKNOWN = 0 +"""Arbitrary coordinates.""" + + NIFTI_XFORM_SCANNER_ANAT = 1 +"""Scanner-based anatomical coordinates.""" + + NIFTI_XFORM_ALIGNED_ANAT = 2 +"""Coordinates aligned to another file's, or to anatomical "truth".""" + + NIFTI_XFORM_TALAIRACH = 3 +"""Coordinates aligned to Talairach-Tournoux Atlas; (0,0,0)=AC, etc.""" + + NIFTI_XFORM_MNI_152 = 4 +"""MNI 152 normalized coordinates.""" diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py index c87121f21..59d409158 100644 --- a/fsl/data/featimage.py +++ b/fsl/data/featimage.py @@ -5,10 +5,10 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """This module provides the :class:`FEATImage` class, a subclass of -:class:`.Image` designed for the ``filtered_func_data`` file of a FEAT -analysis. +:class:`.Image` designed to encapsulate data from a FEAT analysis. """ + import os.path as op import numpy as np @@ -18,11 +18,49 @@ import featresults class FEATImage(fslimage.Image): + """An ``Image`` from a FEAT analysis. + + The :class:`FEATImage` class makes use of the functions defined in the + :mod:`.featresults` module. + + + An example of using the ``FEATImage`` class:: + + import fsl.data.featimage as featimage + + # You can pass in the name of the + # .feat/.gfeat directory, or any + # file contained within that directory. + img = featimage.FEATImage('myanalysis.feat/filtered_func_data.nii.gz') + + # Query information about the FEAT analysis + print img.numEVs() + print img.contrastNames() + print img.numPoints() + + # Get the model fit residuals + res4d = img.getResiduals() + + # Get the full model fit for voxel + # [23, 30, 42] (in this example, we + # have 4 EVs - the first argument + # is a contrast vector). + img.fit([1, 1, 1, 1], [23, 30, 42], fullmodel=True) + """ + def __init__(self, path, **kwargs): - """ - The specified ``path`` may be a FEAT analysis directory, or the model - data input file (e.g. ``analysis.feat/filtered_func_data.nii.gz``). + """Create a ``FEATImage`` instance. + + :arg path: A FEAT analysis directory, or an image file contained + within such a directory. + + :arg kwargs: Passed to the :meth:`.Image.__init__` constructor. + + .. note:: If a FEAT directory is passed in for the ``path`` + argument, this ``FEATImage`` instance will encapsulate + the model input data, typically called + ``<directory>.feat/filtered_func_data.nii.gz``. """ featDir = featresults.getFEATDir(path) @@ -59,53 +97,82 @@ class FEATImage(fslimage.Image): def getFEATDir(self): + """Returns the FEAT directory this image is contained in.""" return self.__featDir def getAnalysisName(self): + """Returns the FEAT analysis name, which is the FEAT directory + name, minus the ``.feat`` / ``.gfeat`` suffix. + """ return self.__analysisName def getDesign(self): + """Returns the analysis design matrix as a :mod:`numpy` array + with shape :math:`numPoints\\times numEVs`. + """ return np.array(self.__design) def numPoints(self): + """Returns the number of points (e.g. time points, number of + subjects, etc) in the analysis. + """ return self.__design.shape[0] def numEVs(self): + """Returns the number of explanatory variables (EVs) in the analysis. + """ return self.__design.shape[1] def evNames(self): + """Returns a list containing the name of each EV in the analysis.""" return list(self.__evNames) def numContrasts(self): + """Returns the number of contrasts in the analysis.""" return len(self.__contrasts) def contrastNames(self): + """Returns a list containing the name of each contrast in the analysis. + """ return list(self.__contrastNames) def contrasts(self): + """Returns a list containing the analysis contrast vectors. + + See :func:`.featresults.loadContrasts` + + """ return [list(c) for c in self.__contrasts] def thresholds(self): + """Returns the statistical thresholds used in the analysis. + + See :func:`.featresults.getThresholds` + """ return featresults.getThresholds(self.__settings) def clusterResults(self, contrast): + """Returns the clusters found in the analysis. + See :func:.featresults.loadClusterResults` + """ return featresults.loadClusterResults(self.__featDir, self.__settings, contrast) def getPE(self, ev): + """Returns the PE image for the given EV (0-indexed). """ if self.__pes[ev] is None: pefile = featresults.getPEFile(self.__featDir, ev) @@ -120,6 +187,7 @@ class FEATImage(fslimage.Image): def getResiduals(self): + """Returns the residuals of the full model fit. """ if self.__residuals is None: resfile = featresults.getResidualFile(self.__featDir) @@ -131,6 +199,7 @@ class FEATImage(fslimage.Image): def getCOPE(self, con): + """Returns the COPE image for the given contrast (0-indexed). """ if self.__copes[con] is None: copefile = featresults.getPEFile(self.__featDir, con) @@ -145,6 +214,8 @@ class FEATImage(fslimage.Image): def getZStats(self, con): + """Returns the Z statistic image for the given contrast (0-indexed). + """ if self.__zstats[con] is None: zfile = featresults.getZStatFile(self.__featDir, con) @@ -160,6 +231,8 @@ class FEATImage(fslimage.Image): def getClusterMask(self, con): + """Returns the cluster mask image for the given contrast (0-indexed). + """ if self.__clustMasks[con] is None: mfile = featresults.getClusterMaskFile(self.__featDir, con) @@ -175,12 +248,22 @@ class FEATImage(fslimage.Image): def fit(self, contrast, xyz, fullmodel=False): - """ + """Calculates the model fit for the given contrast vector + at the given voxel. Passing in a contrast of all 1s, and ``fullmodel=True`` will get you the full model fit. Pass in ``fullmodel=False`` for all other contrasts, otherwise the model fit values will not be scaled correctly. + + :arg contrast: The contrast vector (pass all 1s for a full model + fit). + + :arg xyz: Coordinates of the voxel to calculate the model fit + for. + + :arg fullmodel: Set to ``True`` for a full model fit, ``False`` + otherwise. """ if not fullmodel: @@ -205,11 +288,11 @@ class FEATImage(fslimage.Image): return modelfit + data.mean() - def reducedData(self, xyz, contrast, fullmodel=False): - """ + def partialFit(self, contrast, xyz, fullmodel=False): + """Calculates and returns the partial model fit for the specified + contrast vector at the specified voxel. - Passing in a contrast of all 1s, and ``fullmodel=True`` will - get you the model fit residuals. + See :meth:`fit` for details on the arguments. """ x, y, z = xyz diff --git a/fsl/data/featresults.py b/fsl/data/featresults.py index 4d3ae675f..1a65c2ce9 100644 --- a/fsl/data/featresults.py +++ b/fsl/data/featresults.py @@ -6,7 +6,34 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """This module provides a few utility functions for loading/querying the -contents of a FEAT analysis directory. +contents of a FEAT analysis directory. They are primarily for use by the +:class:`.FEATImage` class, but are available for other uses if needed. The +following functions are provided: + +.. autosummary:: + :nosignatures: + + isFEATDir + getFEATDir + loadDesign + loadContrasts + loadSettings + getEVNames + getThresholds + loadClusterResults + + +The following functions return the names of various files of interest: + +.. autosummary:: + :nosignatures: + + getDataFile + getResidualFile + getPEFile + getCOPEFile + getZStatFile + getClusterMaskFile """ @@ -25,8 +52,9 @@ log = logging.getLogger(__name__) def isFEATDir(path): """Returns ``True`` if the given path looks like a FEAT directory, or looks like the input data for a FEAT analysis, ``False`` otherwise. - """ + :arg path: A file / directory path. + """ dirname, filename = op.split(path) @@ -50,6 +78,15 @@ def isFEATDir(path): def getFEATDir(path): + """Given the path of any file/directory which is within a ``.feat`` or + ``.gfeat`` directory, strips all trailing components of the path name, + and returns the root FEAT directory. + + Returns ``None`` if the given path is not contained within a ``.feat`` + or ``.gfeat`` directory. + + :arg path: A file / directory path. + """ sufs = ['.feat', '.gfeat'] idxs = [(path.rfind(s), s) for s in sufs] @@ -68,10 +105,12 @@ def getFEATDir(path): def loadDesign(featdir): - """Loads the design matrix from a FEAT folder. + """Loads the design matrix from a FEAT directory. Returns a ``numpy`` array containing the design matrix data, where the first dimension corresponds to the data points, and the second to the EVs. + + :arg featdir: A FEAT directory. """ matrix = None @@ -96,11 +135,14 @@ def loadDesign(featdir): def loadContrasts(featdir): - """Loads the contrasts from a FEAT folder. Returns a tuple containing: + """Loads the contrasts from a FEAT directory. Returns a tuple containing: - - A dictionary of ``{contrastnum : name}`` mappings + - A dictionary of ``{contrastnum : name}`` mappings (the ``contrastnum`` + values are 1-indexed). - A list of contrast vectors (each of which is a list itself). + + :arg featdir: A FEAT directory. """ matrix = None @@ -151,9 +193,12 @@ def loadContrasts(featdir): def loadSettings(featdir): - """Loads the analysis settings from a a FEAT folder. + """Loads the analysis settings from a FEAT directory. + + Returns a dict containing the settings specified in the ``design.fsf`` + file within the directory - Returns a dict containing the settings specified in the given file. + :arg featdir: A FEAT directory. """ settings = {} @@ -183,6 +228,18 @@ def loadSettings(featdir): def getThresholds(settings): + """Given a FEAT settings dictionary, returns a dictionary of + ``{stat : threshold}`` mappings, containing the thresholds used + in the FEAT statistical analysis. + + The following keys will be present. Threshold values will be ``None`` + if the respective statistical thresholding was not carried out: + + - ``p``: P-value thresholding + - ``z``: Z-statistic thresholding + + :arg settings: A FEAT settings dictionary (see :func:`loadSettings`). + """ return { 'p' : settings.get('prob_thresh', None), 'z' : settings.get('z_thresh', None) @@ -191,13 +248,43 @@ def getThresholds(settings): def loadClusterResults(featdir, settings, contrast): """If cluster thresholding was used in the FEAT analysis, this function - will load and return the cluster results for the specified contrast - (which is assumed to be 0-indexed). + will load and return the cluster results for the specified (0-indexed) + contrast number. - If there are no cluster results for the given contrast, - ``None`` is returned. + If there are no cluster results for the given contrast, ``None`` is + returned. An error will be raised if the cluster file cannot be parsed. + + :arg featdir: A FEAT directory. + :arg settings: A FEAT settings dictionary. + :arg contrast: 0-indexed contrast number. + + :returns: A list of ``Cluster`` instances, each of which contains + information about one cluster. A ``Cluster`` object has the + following attributes: + + ============ ========================================= + ``index`` Cluster index. + ``nvoxels`` Number of voxels in cluster. + ``p`` Cluster p value. + ``logp`` :math:`-log_{10}` of the cluster P value. + ``zmax`` Maximum Z value in cluster. + ``zmaxx`` X voxel coordinate of maximum Z value. + ``zmaxy`` Y voxel coordinate of maximum Z value. + ``zmaxz`` Z voxel coordinate of maximum Z value. + ``zcogx`` X voxel coordinate of cluster centre of + gravity. + ``zcogy`` Y voxel coordinate of cluster centre of + gravity. + ``zcogz`` Z voxel coordinate of cluster centre of + gravity. + ``copemax`` Maximum COPE value in cluster. + ``copemaxx`` X voxel coordinate of maximum COPE value. + ``copemaxy`` Y voxel coordinate of maximum COPE value. + ``copemaxz`` Z voxel coordinate of maximum COPE value. + ``copemean`` Mean COPE of all voxels in the cluster. + ============ ========================================= """ # Cluster files are named like @@ -209,7 +296,6 @@ def loadClusterResults(featdir, settings, contrast): clusterFile = op.join( featdir, 'cluster_zstat{}.txt'.format(contrast + 1)) - if not op.exists(clusterFile): # If the analysis was performed in standard @@ -334,8 +420,10 @@ def loadClusterResults(featdir, settings, contrast): def getDataFile(featdir): - """Returns the name of the file in the FEAT results which contains + """Returns the name of the file in the FEAT directory which contains the model input data (typically called ``filtered_func_data.nii.gz``). + + :arg featdir: A FEAT directory. """ # Assuming here that there is only @@ -346,6 +434,8 @@ def getDataFile(featdir): def getResidualFile(featdir): """Returns the name of the file in the FEAT results which contains the model fit residuals (typically called ``res4d.nii.gz``). + + :arg featdir: A FEAT directory. """ # Assuming here that there is only @@ -354,33 +444,40 @@ def getResidualFile(featdir): def getPEFile(featdir, ev): - """Returns the path of the PE file for the specified ``ev``, which is - assumed to be 0-indexed. - """ + """Returns the path of the PE file for the specified EV. + :arg featdir: A FEAT directory. + :arg ev: The EV number (0-indexed). + """ pefile = op.join(featdir, 'stats', 'pe{}.*'.format(ev + 1)) return glob.glob(pefile)[0] def getCOPEFile(featdir, contrast): - """Returns the path of the COPE file for the specified ``contrast``, which - is assumed to be 0-indexed. + """Returns the path of the COPE file for the specified contrast. + + :arg featdir: A FEAT directory. + :arg contrast: The contrast number (0-indexed). """ copefile = op.join(featdir, 'stats', 'cope{}.*'.format(contrast + 1)) return glob.glob(copefile)[0] def getZStatFile(featdir, contrast): - """Returns the path of the Z-statistic file for the specified - ``contrast``, which is assumed to be 0-indexed. + """Returns the path of the Z-statistic file for the specified contrast. + + :arg featdir: A FEAT directory. + :arg contrast: The contrast number (0-indexed). """ zfile = op.join(featdir, 'stats', 'zstat{}.*'.format(contrast + 1)) return glob.glob(zfile)[0] def getClusterMaskFile(featdir, contrast): - """Returns the path of the cluster mask file for the specified - ``contrast``, which is assumed to be 0-indexed. + """Returns the path of the cluster mask file for the specified contrast. + + :arg featdir: A FEAT directory. + :arg contrast: The contrast number (0-indexed). """ mfile = op.join(featdir, 'cluster_mask_zstat{}.*'.format(contrast + 1)) return glob.glob(mfile)[0] @@ -392,6 +489,8 @@ def getEVNames(settings): An error of some sort will be raised if the EV names cannot be determined from the FEAT settings. + + :arg settings: A FEAT settings dictionary (see :func:`loadSettings`). """ numEVs = int(settings['evs_real']) diff --git a/fsl/data/image.py b/fsl/data/image.py index 87ae8d265..2547614fd 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -5,9 +5,32 @@ # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -"""Provides the :class:`Image` class, for representing 3D/4D NIFTI images. +"""This module provides the :class:`Image` class, for representing 3D/4D NIFTI1 +images. The ``nibabel`` package is used for file I/O. + +.. note:: Currently, only NIFTI1 images are supported. + + +It is very easy to load a NIFTI image:: + + from fsl.data.image import Image + myimg = Image('MNI152_T1_2mm.nii.gz') + + +A number of other functions are also provided for working with image files and +file names: + +.. autosummary:: + :nosignatures: + + isSupported + removeExt + addExt + loadImage + saveImage """ + import logging import tempfile import os @@ -28,30 +51,28 @@ class Image(props.HasProperties): """Class which represents a 3D/4D image. Internally, the image is loaded/stored using :mod:`nibabel`. - In addition to the class-level properties defined below, the following - attributes are present on an :class:`Image` object: - - :ivar nibImage: The :mod:`nibabel` image object. - - :ivar shape: A list/tuple containing the number of voxels - along each image dimension. - - :ivar pixdim: A list/tuple containing the size of one voxel - along each image dimension. - - :ivar voxToWorldMat: A 4*4 array specifying the affine transformation - for transforming voxel coordinates into real world - coordinates. - - :ivar worldToVoxMat: A 4*4 array specifying the affine transformation - for transforming real world coordinates into voxel - coordinates. - - :ivar dataSource: The name of the file that the image was loaded from. - - :ivar tempFile: The name of the temporary file which was created (in - the event that the image was large and was gzipped - - see :func:`_loadImageFile`). + + In addition to the :attr:`name`, :attr:`data`, and :attr:`saved` + properties, the following attributes are present on an ``Image`` instance: + + + ================= ==================================================== + ``nibImage`` The :mod:`nibabel` image object. + ``dataSource`` The name of the file that the image was loaded from. + ``tempFile`` The name of the temporary file which was created (in + the event that the image was large and was gzipped - + see :func:`loadImage`). + ``shape`` A list/tuple containing the number of voxels along + each image dimension. + ``pixdim`` A list/tuple containing the size of one voxel along + each image dimension. + ``voxToWorldMat`` A 4*4 array specifying the affine transformation + for transforming voxel coordinates into real world + coordinates. + ``worldToVoxMat`` A 4*4 array specifying the affine transformation + for transforming real world coordinates into voxel + coordinates. + ================= ==================================================== """ @@ -77,14 +98,15 @@ class Image(props.HasProperties): name=None, header=None, loadData=True): - """Initialise an Image object with the given image data or file name. + """Create an ``Image`` object with the given image data or file name. :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 xform: A ``4*4`` affine transformation matrix which transforms - voxel coordinates into real world coordinates. + :arg xform: A :math:`4\\times 4` affine transformation matrix + which transforms voxel coordinates into real world + coordinates. :arg name: A name for the image. @@ -100,9 +122,13 @@ class Image(props.HasProperties): via the :meth:`loadData` method. """ - self.nibImage = None - self.dataSource = None - self.tempFile = None + self.nibImage = None + self.dataSource = None + self.tempFile = None + self.shape = None + self.pixdim = None + self.voxToWorldMat = None + self.worldToVoxMat = None if header is not None: header = header.copy() @@ -173,8 +199,28 @@ class Image(props.HasProperties): def __del__(self): + """Prints a log message. """ log.memory('{}.del ({})'.format(type(self).__name__, id(self))) + + + def __hash__(self): + """Returns a number which uniquely idenfities this ``Image`` instance + (the result of ``id(self)``). + """ + return id(self) + + + def __str__(self): + """Return a string representation of this ``Image`` instance.""" + return '{}({}, {})'.format(self.__class__.__name__, + self.name, + self.dataSource) + + def __repr__(self): + """See the :meth:`__str__` method.""" + return self.__str__() + def loadData(self): """Loads the image data from the file. This method only needs to @@ -253,28 +299,8 @@ class Image(props.HasProperties): return saveImage(self) - def __hash__(self): - """Returns a number which uniquely idenfities this :class:`Image` - object (the result of ``id(self)``). - """ - return id(self) - - - def __str__(self): - """Return a string representation of this :class:`Image`.""" - return '{}({}, {})'.format(self.__class__.__name__, - self.name, - self.dataSource) - - - def __repr__(self): - """See the :meth:`__str__` method.""" - return self.__str__() - - def is4DImage(self): - """Returns ``True`` if this image is 4D, ``False`` otherwise. - """ + """Returns ``True`` if this image is 4D, ``False`` otherwise. """ return len(self.shape) > 3 and self.shape[3] > 1 @@ -283,7 +309,14 @@ class Image(props.HasProperties): indicating the space to which the (transformed) image is oriented. The ``code`` parameter may be either ``sform`` (the default) or - ``qform`` in which case the corresponding matrix is used. + ``qform`` in which case the corresponding matrix is used. + + :returns: one of the following codes: + - :data:`~.constants.NIFTI_XFORM_UNKNOWN` + - :data:`~.constants.NIFTI_XFORM_SCANNER_ANAT` + - :data:`~.constants.NIFTI_XFORM_ALIGNED_ANAT` + - :data:`~.constants.NIFTI_XFORM_TALAIRACH` + - :data:`~.constants.NIFTI_XFORM_MNI_152` """ if code is None: code = 'sform_code' @@ -306,21 +339,22 @@ class Image(props.HasProperties): This method returns one of the following values, indicating the direction in which coordinates along the specified axis increase: - - :attr:`~fsl.data.constants.ORIENT_L2R`: Left to right - - :attr:`~fsl.data.constants.ORIENT_R2L`: Right to left - - :attr:`~fsl.data.constants.ORIENT_A2P`: Anterior to posterior - - :attr:`~fsl.data.constants.ORIENT_P2A`: Posterior to anterior - - :attr:`~fsl.data.constants.ORIENT_I2S`: Inferior to superior - - :attr:`~fsl.data.constants.ORIENT_S2I`: Superior to inferior - - :attr:`~fsl.data.constants.ORIENT_UNKNOWN`: Orientation is unknown + + - :attr:`~.constants.ORIENT_L2R`: Left to right + - :attr:`~.constants.ORIENT_R2L`: Right to left + - :attr:`~.constants.ORIENT_A2P`: Anterior to posterior + - :attr:`~.constants.ORIENT_P2A`: Posterior to anterior + - :attr:`~.constants.ORIENT_I2S`: Inferior to superior + - :attr:`~.constants.ORIENT_S2I`: Superior to inferior + - :attr:`~.constants.ORIENT_UNKNOWN`: Orientation is unknown The returned value is dictated by the XForm code contained in the - image file header (see the :meth:`getXFormCode` method). Basically, - if the XForm code is 'unknown', this method will return -1 for all - axes. Otherwise, it is assumed that the image is in RAS orientation - (i.e. the X axis increases from left to right, the Y axis increases - from posterior to anterior, and the Z axis increases from inferior - to superior). + image file header (see the :meth:`getXFormCode` method). Basically, if + the XForm code is *unknown*, this method will return + ``ORIENT_UNKNOWN`` for all axes. Otherwise, it is assumed that the + image is in RAS orientation (i.e. the X axis increases from left to + right, the Y axis increases from posterior to anterior, and the Z axis + increases from inferior to superior). """ if self.getXFormCode(code) == constants.NIFTI_XFORM_UNKNOWN: @@ -335,7 +369,10 @@ class Image(props.HasProperties): def getVoxelOrientation(self, axis, code=None): """Returns a code representing the (estimated) orientation of the - specified voxelwise axis. + specified data axis. + + :arg code: May be either ``qform`` or ``sform``, specifying which + transformation to use. See the :meth:`getWorldOrientation` method for a description of the return value. @@ -368,10 +405,11 @@ class Image(props.HasProperties): # so i'm just providing '*.gz'for now ALLOWED_EXTENSIONS = ['.nii.gz', '.nii', '.img', '.hdr', '.img.gz', '.gz'] """The file extensions which we understand. This list is used as the default -if if the ``allowedExts`` parameter is not passed to any of the functions +if the ``allowedExts`` parameter is not passed to any of the functions below. """ + EXTENSION_DESCRIPTIONS = ['Compressed NIFTI1 images', 'NIFTI1 images', 'ANALYZE75 images', @@ -386,8 +424,7 @@ DEFAULT_EXTENSION = '.nii.gz' def isSupported(filename, allowedExts=None): - """ - Returns ``True`` if the given file has a supported extension, ``False`` + """Returns ``True`` if the given file has a supported extension, ``False`` otherwise. :arg filename: The file name to test. @@ -402,8 +439,7 @@ def isSupported(filename, allowedExts=None): def removeExt(filename, allowedExts=None): - """ - Removes the extension from the given file name. Returns the filename + """Removes the extension from the given file name. Returns the filename unmodified if it does not have a supported extension. :arg filename: The file name to strip. @@ -429,11 +465,7 @@ def removeExt(filename, allowedExts=None): return filename[:-extLen] -def addExt( - prefix, - mustExist=True, - allowedExts=None, - defaultExt=None): +def addExt(prefix, mustExist=True, allowedExts=None, defaultExt=None): """Adds a file extension to the given file ``prefix``. If ``mustExist`` is False, and the file does not already have a @@ -571,9 +603,9 @@ def saveImage(image, fromDir=None): the image. - :param image: The :class:`.Image` instance to be saved. + :arg image: The :class:`.Image` instance to be saved. - :param str fromDir: Directory in which the file dialog should start. + :arg fromDir: Directory in which the file dialog should start. If ``None``, the most recently visited directory (via this method) is used, or the directory from the given image, or the current working directory. diff --git a/fsl/data/model.py b/fsl/data/model.py index 1598f73db..a336304ba 100644 --- a/fsl/data/model.py +++ b/fsl/data/model.py @@ -1,9 +1,20 @@ #!/usr/bin/env python # -# model.py - +# model.py - The Model class, for VTK polygon data. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module providse the :class:`Model` class, which represents a 3D model. + +.. note:: I/O support is very limited - currently, the only supported file + type is the VTK legacy file format, containing the ``POLYDATA`` + dataset. the :class:`Model` class assumes that every polygon defined + in an input file is a triangle (i.e. refers to three vertices). + + See http://www.vtk.org/wp-content/uploads/2015/04/file-formats.pdf + for an overview of the VTK legacy file format. +""" + import logging @@ -14,53 +25,22 @@ import numpy as np log = logging.getLogger(__name__) -ALLOWED_EXTENSIONS = ['.vtk'] -EXTENSION_DESCRIPTIONS = ['VTK polygon model file'] - - -def loadVTKPolydataFile(infile): - - lines = None - - with open(infile, 'rt') as f: - lines = f.readlines() - - lines = [l.strip() for l in lines] - - if lines[3] != 'DATASET POLYDATA': - raise ValueError('') - - nVertices = int(lines[4].split()[1]) - nPolygons = int(lines[5 + nVertices].split()[1]) - nIndices = int(lines[5 + nVertices].split()[2]) - nPolygons - - vertices = np.zeros((nVertices, 3), dtype=np.float32) - polygonLengths = np.zeros( nPolygons, dtype=np.uint32) - indices = np.zeros( nIndices, dtype=np.uint32) - - for i in range(nVertices): - vertLine = lines[i + 5] - vertices[i, :] = map(float, vertLine.split()) - - indexOffset = 0 - for i in range(nPolygons): - - polyLine = lines[6 + nVertices + i].split() - polygonLengths[i] = int(polyLine[0]) - - start = indexOffset - end = indexOffset + polygonLengths[i] - indices[start:end] = map(int, polyLine[1:]) - - indexOffset += polygonLengths[i] +class Model(object): + """The ``Model`` class represents a 3D model. A model is defined by a + collection of vertices and indices. The indices index into the list of + vertices, and define a set of triangles which make the model. + """ - return vertices, polygonLengths, indices + def __init__(self, data, indices=None): + """Create a ``Model`` instance. -class Model(object): + :arg data: Can either be a file name, or a :math:`N\\times 3` + ``numpy`` array containing vertex data. If ``data`` is + a file name, it is passed to the + :func:`loadVTKPolydataFile` function. - def __init__(self, data, indices=None): - """ + :arg indices: A list of indices into the vertex data. """ if isinstance(data, basestring): @@ -90,17 +70,85 @@ class Model(object): def __del__(self): + """Prints a log message.""" log.memory('{}.del ({})'.format(type(self).__name__, id(self))) def __repr__(self): + """Rewturns a string representation of this ``Model`` instance. """ return '{}({}, {})'.format(type(self).__name__, self.name, self.dataSource) def __str__(self): + """Rewturns a string representation of this ``Model`` instance. """ return self.__repr__() def getBounds(self): + """Returns a tuple of values which define a minimal bounding box that + will contain all vertices in this ``Model`` instance. The bounding + box is arranged like so: + + ``((xlow, ylow, zlow), (xhigh, yhigh, zhigh))`` + """ return (self.__loBounds, self.__hiBounds) + + +ALLOWED_EXTENSIONS = ['.vtk'] +"""A list of file extensions which could contain :class:`Model` data. """ + + +EXTENSION_DESCRIPTIONS = ['VTK polygon model file'] +"""A description for each of the extensions in :data:`ALLOWED_EXTENSIONS`.""" + + +def loadVTKPolydataFile(infile): + """Loads a vtk legacy file containing a ``POLYDATA`` data set. + + :arg infile: Name of a file to load from. + + :returns: a tuple containing three values: + + - A :math:`N\\times 3` ``numpy`` array containing :math:`N` + vertices. + - A 1D ``numpy`` array containing the lengths of each polygon. + - A 1D ``numpy`` array containing the vertex indices for all + polygons. + """ + + lines = None + + with open(infile, 'rt') as f: + lines = f.readlines() + + lines = [l.strip() for l in lines] + + if lines[3] != 'DATASET POLYDATA': + raise ValueError('Only the POLYDATA data type is supported') + + nVertices = int(lines[4].split()[1]) + nPolygons = int(lines[5 + nVertices].split()[1]) + nIndices = int(lines[5 + nVertices].split()[2]) - nPolygons + + vertices = np.zeros((nVertices, 3), dtype=np.float32) + polygonLengths = np.zeros( nPolygons, dtype=np.uint32) + indices = np.zeros( nIndices, dtype=np.uint32) + + for i in range(nVertices): + vertLine = lines[i + 5] + vertices[i, :] = map(float, vertLine.split()) + + indexOffset = 0 + for i in range(nPolygons): + + polyLine = lines[6 + nVertices + i].split() + polygonLengths[i] = int(polyLine[0]) + + start = indexOffset + end = indexOffset + polygonLengths[i] + indices[start:end] = map(int, polyLine[1:]) + + indexOffset += polygonLengths[i] + + return vertices, polygonLengths, indices diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 127a89b4d..df9fd6a79 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -4,10 +4,31 @@ # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module contains a collection of strings used throughout ``fslpy`` for +display purposes. Most of the strings are used by FSLeyes. + + +The strings are stored in :class:`.TypeDict` dictionaries, roughly organised +into the following categories: + + + - :data:`messages`: Messages to be displayed to the user. + - :data:`titles`: Titles of windows, panels, and dialogs. + - :data:`actions`: Names of actions tied to menu options, buttons, etc. + - :data:`labels`: Labels for miscellaneous things. + - :data:`properties`: Display names for ``props.HasProperties`` properties. + - :data:`choices`: Display names for ``props.HasProperties`` choice + properties. + - :data:`anatomy`: Anatomical and orientation labels. + - :data:`nifti`: Labels for NIFTI header fields. + - :data:`feat`: FEAT specific names and labels. +""" + from fsl.utils.typedict import TypeDict import fsl.data.constants as constants + messages = TypeDict({ 'FSLDirDialog.FSLDirNotSet' : 'The $FSLDIR environment variable ' @@ -101,7 +122,6 @@ messages = TypeDict({ }) - titles = TypeDict({ 'FSLDirDialog' : '$FSLDIR is not set', @@ -202,6 +222,7 @@ actions = TypeDict({ 'OrthoEditProfile.createROIFromSelection' : 'ROI', }) + labels = TypeDict({ 'FSLDirDialog.locate' : 'Locate $FSLDIR', diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py index f8e3d9072..63f058e52 100644 --- a/fsl/fsleyes/views/timeseriespanel.py +++ b/fsl/fsleyes/views/timeseriespanel.py @@ -343,7 +343,7 @@ class FEATReducedTimeSeries(TimeSeries): def getData(self): - data = self.overlay.reducedData(self.coords, self.contrast, False) + data = self.overlay.partialFit(self.contrast, self.coords, False) return TimeSeries.getData(self, ydata=data) diff --git a/fsl/utils/colourbarbitmap.py b/fsl/utils/colourbarbitmap.py index 25aeed4c3..3199b4eef 100644 --- a/fsl/utils/colourbarbitmap.py +++ b/fsl/utils/colourbarbitmap.py @@ -50,7 +50,7 @@ def colourBarBitmap(cmap, :arg label: Text label to show next to the colour bar. - :arg orientation: Either ``vertical`` or `horizontal``. + :arg orientation: Either ``vertical`` or ``horizontal``. :arg labelside: If ``orientation`` is ``vertical`` ``labelSide`` may be either ``left`` or ``right``. Otherwise, if -- GitLab