diff --git a/docs/api.rst b/docs/api.rst index 38c16945e..01442a50c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -139,14 +139,14 @@ .. _calibration_ref: -:mod:`tedana.io`: Utility functions +:mod:`tedana.io`: IO functions -------------------------------------------------- -.. automodule:: tedana.utils +.. automodule:: tedana.io :no-members: :no-inherited-members: -.. autosummary:: tedana.utils +.. autosummary:: tedana.io :toctree: generated/ :template: module.rst diff --git a/tedana/io.py b/tedana/io.py index 9914666e3..bc8ceea64 100644 --- a/tedana/io.py +++ b/tedana/io.py @@ -13,6 +13,7 @@ from numpy.linalg import lstsq from tedana import model, utils +from tedana.utils import load_image LGR = logging.getLogger(__name__) @@ -593,3 +594,54 @@ def filewrite(data, filename, ref_img, gzip=False, copy_header=True): out.to_filename(name) return name + + +def load_data(data, n_echos=None): + """ + Coerces input `data` files to required 3D array output + + Parameters + ---------- + data : (X x Y x M x T) array_like or :obj:`list` of img_like + Input multi-echo data array, where `X` and `Y` are spatial dimensions, + `M` is the Z-spatial dimensions with all the input echos concatenated, + and `T` is time. A list of image-like objects (e.g., .nii) are + accepted, as well + n_echos : :obj:`int`, optional + Number of echos in provided data array. Only necessary if `data` is + array_like. Default: None + + Returns + ------- + fdata : (S x E x T) :obj:`numpy.ndarray` + Output data where `S` is samples, `E` is echos, and `T` is time + ref_img : :obj:`str` or :obj:`numpy.ndarray` + Filepath to reference image for saving output files or NIFTI-like array + """ + if n_echos is None: + raise ValueError('Number of echos must be specified. ' + 'Confirm that TE times are provided with the `-e` argument.') + + if isinstance(data, list): + if len(data) == 1: # a z-concatenated file was provided + data = data[0] + elif len(data) == 2: # inviable -- need more than 2 echos + raise ValueError('Cannot run `tedana` with only two echos: ' + '{}'.format(data)) + else: # individual echo files were provided (surface or volumetric) + fdata = np.stack([load_image(f) for f in data], axis=1) + ref_img = check_niimg(data[0]) + ref_img.header.extensions = [] + return np.atleast_3d(fdata), ref_img + + img = check_niimg(data) + (nx, ny), nz = img.shape[:2], img.shape[2] // n_echos + fdata = load_image(img.get_data().reshape(nx, ny, nz, n_echos, -1, order='F')) + + # create reference image + ref_img = img.__class__(np.zeros((nx, ny, nz)), affine=img.affine, + header=img.header, extra=img.extra) + ref_img.header.extensions = [] + ref_img.header.set_sform(ref_img.header.get_sform(), code=1) + + return fdata, ref_img diff --git a/tedana/tests/test_decay.py b/tedana/tests/test_decay.py index 67bfbad3f..e72e6d805 100644 --- a/tedana/tests/test_decay.py +++ b/tedana/tests/test_decay.py @@ -7,7 +7,7 @@ import numpy as np import pytest -from tedana import utils, decay as me +from tedana import io, utils, decay as me from tedana.tests.utils import get_test_data_path @@ -16,7 +16,7 @@ def testdata1(): tes = np.array([14.5, 38.5, 62.5]) in_files = [op.join(get_test_data_path(), 'echo{0}.nii.gz'.format(i+1)) for i in range(3)] - data, _ = utils.load_data(in_files, n_echos=len(tes)) + data, _ = io.load_data(in_files, n_echos=len(tes)) mask, mask_sum = utils.make_adaptive_mask(data, minimum=False, getsum=True) data_dict = {'data': data, 'tes': tes, diff --git a/tedana/tests/test_io.py b/tedana/tests/test_io.py index da36fc229..2d39a85ed 100644 --- a/tedana/tests/test_io.py +++ b/tedana/tests/test_io.py @@ -3,15 +3,16 @@ """ import nibabel as nib +import numpy as np +import pytest -import tedana.io -from tedana import utils +from tedana import io as me from tedana.tests.test_utils import fnames, tes def test_new_nii_like(): - data, ref = utils.load_data(fnames, n_echos=len(tes)) - nimg = tedana.io.new_nii_like(ref, data) + data, ref = me.load_data(fnames, n_echos=len(tes)) + nimg = me.new_nii_like(ref, data) assert isinstance(nimg, nib.Nifti1Image) assert nimg.shape == (39, 50, 33, 3, 5) @@ -19,3 +20,29 @@ def test_new_nii_like(): def test_filewrite(): pass + + +def test_load_data(): + fimg = [nib.load(f) for f in fnames] + exp_shape = (64350, 3, 5) + + # list of filepath to images + d, ref = me.load_data(fnames, n_echos=len(tes)) + assert d.shape == exp_shape + assert isinstance(ref, nib.Nifti1Image) + assert np.allclose(ref.get_data(), nib.load(fnames[0]).get_data()) + + # list of img_like + d, ref = me.load_data(fimg, n_echos=len(tes)) + assert d.shape == exp_shape + assert isinstance(ref, nib.Nifti1Image) + assert ref == fimg[0] + + # imagine z-cat img + d, ref = me.load_data(fnames[0], n_echos=3) + assert d.shape == (21450, 3, 5) + assert isinstance(ref, nib.Nifti1Image) + assert ref.shape == (39, 50, 11) + + with pytest.raises(ValueError): + me.load_data(fnames[0]) diff --git a/tedana/tests/test_utils.py b/tedana/tests/test_utils.py index d011fd0aa..4f1ebeb49 100644 --- a/tedana/tests/test_utils.py +++ b/tedana/tests/test_utils.py @@ -8,7 +8,7 @@ import numpy as np import pytest -from tedana import utils +from tedana import (utils, io) rs = np.random.RandomState(1234) datadir = pjoin(dirname(__file__), 'data') @@ -118,35 +118,9 @@ def test_load_image(): assert utils.load_image(fimg.get_data()).shape == exp_shape -def test_load_data(): - fimg = [nib.load(f) for f in fnames] - exp_shape = (64350, 3, 5) - - # list of filepath to images - d, ref = utils.load_data(fnames, n_echos=len(tes)) - assert d.shape == exp_shape - assert isinstance(ref, nib.Nifti1Image) - assert np.allclose(ref.get_data(), nib.load(fnames[0]).get_data()) - - # list of img_like - d, ref = utils.load_data(fimg, n_echos=len(tes)) - assert d.shape == exp_shape - assert isinstance(ref, nib.Nifti1Image) - assert ref == fimg[0] - - # imagine z-cat img - d, ref = utils.load_data(fnames[0], n_echos=3) - assert d.shape == (21450, 3, 5) - assert isinstance(ref, nib.Nifti1Image) - assert ref.shape == (39, 50, 11) - - with pytest.raises(ValueError): - utils.load_data(fnames[0]) - - def test_make_adaptive_mask(): # load data make masks - data = utils.load_data(fnames, n_echos=len(tes))[0] + data = io.load_data(fnames, n_echos=len(tes))[0] minmask = utils.make_adaptive_mask(data) mask, masksum = utils.make_adaptive_mask(data, minimum=False, getsum=True) @@ -174,7 +148,7 @@ def test_make_adaptive_mask(): def test_make_min_mask(): # load data make mask - data = utils.load_data(fnames, n_echos=len(tes))[0] + data = io.load_data(fnames, n_echos=len(tes))[0] minmask = utils.make_min_mask(data) assert minmask.shape == (64350,) diff --git a/tedana/utils.py b/tedana/utils.py index ce9c5effc..1b97a7731 100644 --- a/tedana/utils.py +++ b/tedana/utils.py @@ -98,57 +98,6 @@ def load_image(data): return fdata -def load_data(data, n_echos=None): - """ - Coerces input `data` files to required 3D array output - - Parameters - ---------- - data : (X x Y x M x T) array_like or :obj:`list` of img_like - Input multi-echo data array, where `X` and `Y` are spatial dimensions, - `M` is the Z-spatial dimensions with all the input echos concatenated, - and `T` is time. A list of image-like objects (e.g., .nii) are - accepted, as well - n_echos : :obj:`int`, optional - Number of echos in provided data array. Only necessary if `data` is - array_like. Default: None - - Returns - ------- - fdata : (S x E x T) :obj:`numpy.ndarray` - Output data where `S` is samples, `E` is echos, and `T` is time - ref_img : :obj:`str` or :obj:`numpy.ndarray` - Filepath to reference image for saving output files or NIFTI-like array - """ - if n_echos is None: - raise ValueError('Number of echos must be specified. ' - 'Confirm that TE times are provided with the `-e` argument.') - - if isinstance(data, list): - if len(data) == 1: # a z-concatenated file was provided - data = data[0] - elif len(data) == 2: # inviable -- need more than 2 echos - raise ValueError('Cannot run `tedana` with only two echos: ' - '{}'.format(data)) - else: # individual echo files were provided (surface or volumetric) - fdata = np.stack([load_image(f) for f in data], axis=1) - ref_img = check_niimg(data[0]) - ref_img.header.extensions = [] - return np.atleast_3d(fdata), ref_img - - img = check_niimg(data) - (nx, ny), nz = img.shape[:2], img.shape[2] // n_echos - fdata = load_image(img.get_data().reshape(nx, ny, nz, n_echos, -1, order='F')) - - # create reference image - ref_img = img.__class__(np.zeros((nx, ny, nz)), affine=img.affine, - header=img.header, extra=img.extra) - ref_img.header.extensions = [] - ref_img.header.set_sform(ref_img.header.get_sform(), code=1) - - return fdata, ref_img - - def make_adaptive_mask(data, mask=None, minimum=True, getsum=False): """ Makes map of `data` specifying longest echo a voxel can be sampled with diff --git a/tedana/workflows/t2smap.py b/tedana/workflows/t2smap.py index 3a1f4baed..f14dfe236 100644 --- a/tedana/workflows/t2smap.py +++ b/tedana/workflows/t2smap.py @@ -154,7 +154,7 @@ def t2smap_workflow(data, tes, mask=None, fitmode='all', combmode='t2s', data = [data] LGR.info('Loading input data: {}'.format([f for f in data])) - catd, ref_img = utils.load_data(data, n_echos=n_echos) + catd, ref_img = io.load_data(data, n_echos=n_echos) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape)) diff --git a/tedana/workflows/tedana.py b/tedana/workflows/tedana.py index 74daa08b6..8a7e36f2a 100644 --- a/tedana/workflows/tedana.py +++ b/tedana/workflows/tedana.py @@ -324,7 +324,7 @@ def tedana_workflow(data, tes, mask=None, mixm=None, ctab=None, manacc=None, data = [data] LGR.info('Loading input data: {}'.format([f for f in data])) - catd, ref_img = utils.load_data(data, n_echos=n_echos) + catd, ref_img = io.load_data(data, n_echos=n_echos) n_samp, n_echos, n_vols = catd.shape LGR.debug('Resulting data shape: {}'.format(catd.shape))