diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d37542fbbbc65888f48418c5c5e5e20384c5bba..31b62178cd2d711fae19645e813e666be314e467 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -29,6 +29,17 @@ Removed * Many deprecated items removed. +Fixed +^^^^^ + + +* Added a missing ``image`` attribute in the :class:`.VoxelwiseConfoundEV` + class. +* Make sure that FEAT ``Cluster`` objects (created by the + :func:`.loadClusterResults` function) contain ``p`` and ``logp`` attributes, + even when cluster thresholding was not used. + + 1.13.0 (Thursday 22nd November 2018) ------------------------------------ @@ -36,9 +47,9 @@ Removed Added ^^^^^ -* New wrapper functions for :func:`.fsl_anat`, :func:`.applytopup` (Matrin +* New wrapper functions for :func:`.fsl_anat`, :func:`.applytopup` (Martin Craig). -* New :func:`.fileOrText` decorator for use in wrapper functions (Matrin +* New :func:`.fileOrText` decorator for use in wrapper functions (Martin Craig). diff --git a/fsl/data/featanalysis.py b/fsl/data/featanalysis.py index 246722b576e47abb5acf9de89cc714c952e1d421..7c38eb7773465fa79cc952e4038c38451e7e3b97 100644 --- a/fsl/data/featanalysis.py +++ b/fsl/data/featanalysis.py @@ -385,6 +385,12 @@ def loadClusterResults(featdir, settings, contrast): setattr(self, attrName, val) + # if cluster thresholding was not used, + # the cluster table will not contain + # P valuse. + if not hasattr(self, 'p'): self.p = 1.0 + if not hasattr(self, 'logp'): self.logp = 0.0 + # This dict provides a mapping between # Cluster object attribute names, and # the corresponding column name in the diff --git a/fsl/data/featdesign.py b/fsl/data/featdesign.py index 68e7e398ed6d066c506f0365482e07daeb1d21f9..633b712b4b04f9de986ae096612987e6814a00a8 100644 --- a/fsl/data/featdesign.py +++ b/fsl/data/featdesign.py @@ -219,6 +219,59 @@ class FEATFSFDesign(object): return design +class VoxelwiseEVMixin(object): + """Mixin class for voxelwise EVs. + + ``VoxelwiseEVMixin`` instances contain the following attributes: + + ============ ====================================================== + ``filename`` Path to the image file containing the data for this EV + ``image`` Reference to the :class:`.Image` object + ============ ====================================================== + """ + + def __init__(self, filename): + """Create a ``VoxelwiseEVMixin``. + + :arg filename: Path to the file containing the data for this + ``VoxelwiseEV``. + """ + if op.exists(filename): + self.__filename = filename + else: + log.warning('Voxelwise EV file does not ' + 'exist: {}'.format(filename)) + self.__filename = None + + self.__image = None + + + def __del__(self): + """Clears any reference to the voxelwise EV image. """ + self.__image = None + + + @property + def filename(self): + """Returns the path to the image file containing the data for this EV. + """ + return self.__filename + + + @property + def image(self): + """Returns the :class:`.Image` containing the voxelwise EV data. """ + + if self.__filename is None: + return None + + if self.__image is not None: + return self.__image + + self.__image = fslimage.Image(self.__filename, mmap=False) + return self.__image + + class EV(object): """Class representing an explanatory variable in a FEAT design matrix. @@ -276,7 +329,7 @@ class BasisFunctionEV(NormalEV): pass -class VoxelwiseEV(NormalEV): +class VoxelwiseEV(NormalEV, VoxelwiseEVMixin): """Class representing an EV with different values for each voxel in the analysis. @@ -308,34 +361,7 @@ class VoxelwiseEV(NormalEV): ``VoxelwiseEV``. """ NormalEV.__init__(self, realIdx, origIdx, title) - - if op.exists(filename): - self.filename = filename - else: - log.warning('Voxelwise EV file does not ' - 'exist: {}'.format(filename)) - self.filename = None - - self.__image = None - - - def __del__(self): - """Clears any reference to the voxelwise EV image. """ - self.__image = None - - - @property - def image(self): - """Returns the :class:`.Image` containing the voxelwise EV data. """ - - if self.filename is None: - return None - - if self.__image is not None: - return self.__image - - self.__image = fslimage.Image(self.filename, mmap=False) - return self.__image + VoxelwiseEVMixin.__init__(self, filename) class ConfoundEV(EV): @@ -386,7 +412,7 @@ class MotionParameterEV(EV): self.motionIndex = motionIndex -class VoxelwiseConfoundEV(EV): +class VoxelwiseConfoundEV(EV, VoxelwiseEVMixin): """Class representing a voxelwise confound EV. ``VoxelwiseConfoundEV`` instances contain the following attributes (in @@ -396,6 +422,7 @@ class VoxelwiseConfoundEV(EV): ``voxIndex`` Index of this ``VoxelwiseConfoundEV`` (starting from 0) in relation to all other voxelwise confound EVs. ``filename`` Path to the image file containing the data for this EV + ``image`` Reference to the :class:`.Image` object ============ ========================================================== """ def __init__(self, index, voxIndex, title, filename): @@ -407,17 +434,13 @@ class VoxelwiseConfoundEV(EV): ``VoxelwiseConfoundEV`` in relation to all other voxelwise confound EVs. :arg title: Name of this ``VoxelwiseConfoundEV``. + :arg filename: Path to the file containing the data for this + ``VoxelwiseConfoundEV``. """ EV.__init__(self, index, title) + VoxelwiseEVMixin.__init__(self, filename) self.voxIndex = voxIndex - if op.exists(filename): - self.filename = filename - else: - log.warning('Voxelwise confound EV file does ' - 'not exist: {}'.format(filename)) - self.filename = None - def getFirstLevelEVs(featDir, settings, designMat): """Derives the EVs for the given first level FEAT analysis. diff --git a/tests/test_featdesign.py b/tests/test_featdesign.py index b10aa23091954d0dd4b840d0c11132d3427bd8ab..dd821127e925bf37a15f17fab5f40d40b5268a26 100644 --- a/tests/test_featdesign.py +++ b/tests/test_featdesign.py @@ -109,8 +109,9 @@ import numpy as np import pytest import tests -import fsl.data.featdesign as featdesign -import fsl.data.featanalysis as featanalysis +from fsl.utils.tempdir import tempdir +import fsl.data.featdesign as featdesign +import fsl.data.featanalysis as featanalysis datadir = op.join(op.dirname(__file__), 'testdata', 'test_feat') @@ -397,3 +398,17 @@ def test_loadDesignMat(): with pytest.raises(Exception): featdesign.loadDesignMat(badfile) + + +def test_VoxelwiseEVs(): + with tempdir(): + img = tests.make_random_image('image.nii.gz', (10, 10, 10, 10)) + + ev1 = featdesign.VoxelwiseEV( 0, 0, 'ev1', 'image.nii.gz') + ev2 = featdesign.VoxelwiseConfoundEV(0, 0, 'ev2', 'image.nii.gz') + + for xyz in tests.random_voxels((10, 10, 10), 10): + x, y, z = map(int, xyz) + exp = img.dataobj[x, y, z, :] + assert np.all(ev1.image[x, y, z, :] == exp) + assert np.all(ev2.image[x, y, z, :] == exp)