/
io.py
406 lines (339 loc) · 15 KB
/
io.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
"""
Functions to handle file input/output
"""
import logging
import os.path as op
import numpy as np
import nibabel as nib
from nibabel.filename_parser import splitext_addext
from nilearn._utils import check_niimg
from nilearn.image import new_img_like
from tedana import utils
from tedana.stats import computefeats2, get_coeffs
LGR = logging.getLogger(__name__)
def split_ts(data, mmix, mask, comptable):
"""
Splits `data` time series into accepted component time series and remainder
Parameters
----------
data : (S x T) array_like
Input data, where `S` is samples and `T` is time
mmix : (T x C) array_like
Mixing matrix for converting input data to component space, where `C`
is components and `T` is the same as in `data`
mask : (S,) array_like
Boolean mask array
comptable : (C x X) :obj:`pandas.DataFrame`
Component metric table. One row for each component, with a column for
each metric. Requires at least two columns: "component" and
"classification".
Returns
-------
hikts : (S x T) :obj:`numpy.ndarray`
Time series reconstructed using only components in `acc`
rest : (S x T) :obj:`numpy.ndarray`
Original data with `hikts` removed
"""
acc = comptable[comptable.classification == 'accepted'].index.values
cbetas = get_coeffs(data - data.mean(axis=-1, keepdims=True),
mmix, mask)
betas = cbetas[mask]
if len(acc) != 0:
hikts = utils.unmask(betas[:, acc].dot(mmix.T[acc, :]), mask)
else:
hikts = None
resid = data - hikts
return hikts, resid
def write_split_ts(data, mmix, mask, comptable, ref_img, suffix=''):
"""
Splits `data` into denoised / noise / ignored time series and saves to disk
Parameters
----------
data : (S x T) array_like
Input time series
mmix : (C x T) array_like
Mixing matrix for converting input data to component space, where `C`
is components and `T` is the same as in `data`
mask : (S,) array_like
Boolean mask array
ref_img : :obj:`str` or img_like
Reference image to dictate how outputs are saved to disk
suffix : :obj:`str`, optional
Appended to name of saved files (before extension). Default: ''
Returns
-------
varexpl : :obj:`float`
Percent variance of data explained by extracted + retained components
Notes
-----
This function writes out several files:
====================== =================================================
Filename Content
====================== =================================================
hik_ts_[suffix].nii High-Kappa time series.
midk_ts_[suffix].nii Mid-Kappa time series.
low_ts_[suffix].nii Low-Kappa time series.
dn_ts_[suffix].nii Denoised time series.
====================== =================================================
"""
acc = comptable[comptable.classification == 'accepted'].index.values
rej = comptable[comptable.classification == 'rejected'].index.values
# mask and de-mean data
mdata = data[mask]
dmdata = mdata.T - mdata.T.mean(axis=0)
# get variance explained by retained components
betas = get_coeffs(dmdata.T, mmix, mask=None)
varexpl = (1 - ((dmdata.T - betas.dot(mmix.T))**2.).sum() /
(dmdata**2.).sum()) * 100
LGR.info('Variance explained by ICA decomposition: {:.02f}%'.format(varexpl))
# create component and de-noised time series and save to files
hikts = betas[:, acc].dot(mmix.T[acc, :])
lowkts = betas[:, rej].dot(mmix.T[rej, :])
dnts = data[mask] - lowkts
if len(acc) != 0:
fout = filewrite(utils.unmask(hikts, mask),
'hik_ts_{0}'.format(suffix), ref_img)
LGR.info('Writing high-Kappa time series: {}'.format(op.abspath(fout)))
if len(rej) != 0:
fout = filewrite(utils.unmask(lowkts, mask),
'lowk_ts_{0}'.format(suffix), ref_img)
LGR.info('Writing low-Kappa time series: {}'.format(op.abspath(fout)))
fout = filewrite(utils.unmask(dnts, mask),
'dn_ts_{0}'.format(suffix), ref_img)
LGR.info('Writing denoised time series: {}'.format(op.abspath(fout)))
return varexpl
def writefeats(data, mmix, mask, ref_img, suffix=''):
"""
Converts `data` to component space with `mmix` and saves to disk
Parameters
----------
data : (S x T) array_like
Input time series
mmix : (C x T) array_like
Mixing matrix for converting input data to component space, where `C`
is components and `T` is the same as in `data`
mask : (S,) array_like
Boolean mask array
ref_img : :obj:`str` or img_like
Reference image to dictate how outputs are saved to disk
suffix : :obj:`str`, optional
Appended to name of saved files (before extension). Default: ''
Returns
-------
fname : :obj:`str`
Filepath to saved file
Notes
-----
This function writes out a file:
====================== =================================================
Filename Content
====================== =================================================
feats_[suffix].nii Z-normalized spatial component maps.
====================== =================================================
"""
# write feature versions of components
feats = utils.unmask(computefeats2(data, mmix, mask), mask)
fname = filewrite(feats, 'feats_{0}'.format(suffix), ref_img)
return fname
def writeresults(ts, mask, comptable, mmix, n_vols, ref_img):
"""
Denoises `ts` and saves all resulting files to disk
Parameters
----------
ts : (S x T) array_like
Time series to denoise and save to disk
mask : (S,) array_like
Boolean mask array
comptable : (C x X) :obj:`pandas.DataFrame`
Component metric table. One row for each component, with a column for
each metric. Requires at least two columns: "component" and
"classification".
mmix : (C x T) array_like
Mixing matrix for converting input data to component space, where `C`
is components and `T` is the same as in `data`
n_vols : :obj:`int`
Number of volumes in original time series
ref_img : :obj:`str` or img_like
Reference image to dictate how outputs are saved to disk
Notes
-----
This function writes out several files:
====================== =================================================
Filename Content
====================== =================================================
ts_OC.nii Optimally combined 4D time series.
hik_ts_OC.nii High-Kappa time series. Generated by
:py:func:`tedana.utils.io.write_split_ts`.
midk_ts_OC.nii Mid-Kappa time series. Generated by
:py:func:`tedana.utils.io.write_split_ts`.
low_ts_OC.nii Low-Kappa time series. Generated by
:py:func:`tedana.utils.io.write_split_ts`.
dn_ts_OC.nii Denoised time series. Generated by
:py:func:`tedana.utils.io.write_split_ts`.
betas_OC.nii Full ICA coefficient feature set.
betas_hik_OC.nii Denoised ICA coefficient feature set.
feats_OC2.nii Z-normalized spatial component maps. Generated
by :py:func:`tedana.utils.io.writefeats`.
comp_table.txt Component table. Generated by
:py:func:`tedana.utils.io.writect`.
====================== =================================================
"""
acc = comptable[comptable.classification == 'accepted'].index.values
fout = filewrite(ts, 'ts_OC', ref_img)
LGR.info('Writing optimally combined time series: {}'.format(op.abspath(fout)))
write_split_ts(ts, mmix, mask, comptable, ref_img, suffix='OC')
ts_B = get_coeffs(ts, mmix, mask)
fout = filewrite(ts_B, 'betas_OC', ref_img)
LGR.info('Writing full ICA coefficient feature set: {}'.format(op.abspath(fout)))
if len(acc) != 0:
fout = filewrite(ts_B[:, acc], 'betas_hik_OC', ref_img)
LGR.info('Writing denoised ICA coefficient feature set: {}'.format(op.abspath(fout)))
fout = writefeats(split_ts(ts, mmix, mask, comptable)[0],
mmix[:, acc], mask, ref_img, suffix='OC2')
LGR.info('Writing Z-normalized spatial component maps: {}'.format(op.abspath(fout)))
def writeresults_echoes(catd, mmix, mask, comptable, ref_img):
"""
Saves individually denoised echos to disk
Parameters
----------
catd : (S x E x T) array_like
Input data time series
mmix : (C x T) array_like
Mixing matrix for converting input data to component space, where `C`
is components and `T` is the same as in `data`
mask : (S,) array_like
Boolean mask array
comptable : (C x X) :obj:`pandas.DataFrame`
Component metric table. One row for each component, with a column for
each metric. The index should be the component number.
ref_img : :obj:`str` or img_like
Reference image to dictate how outputs are saved to disk
Notes
-----
This function writes out several files:
====================== =================================================
Filename Content
====================== =================================================
hik_ts_e[echo].nii High-Kappa timeseries for echo number ``echo``.
Generated by
:py:func:`tedana.utils.io.write_split_ts`.
midk_ts_e[echo].nii Mid-Kappa timeseries for echo number ``echo``.
Generated by
:py:func:`tedana.utils.io.write_split_ts`.
lowk_ts_e[echo].nii Low-Kappa timeseries for echo number ``echo``.
Generated by
:py:func:`tedana.utils.io.write_split_ts`.
dn_ts_e[echo].nii Denoised timeseries for echo number ``echo``.
Generated by
:py:func:`tedana.utils.io.write_split_ts`.
====================== =================================================
"""
for i_echo in range(catd.shape[1]):
LGR.info('Writing Kappa-filtered echo #{:01d} timeseries'.format(i_echo + 1))
write_split_ts(catd[:, i_echo, :], mmix, mask, comptable, ref_img,
suffix='e%i' % (i_echo + 1))
def new_nii_like(ref_img, data, affine=None, copy_header=True):
"""
Coerces `data` into NiftiImage format like `ref_img`
Parameters
----------
ref_img : :obj:`str` or img_like
Reference image
data : (S [x T]) array_like
Data to be saved
affine : (4 x 4) array_like, optional
Transformation matrix to be used. Default: `ref_img.affine`
copy_header : :obj:`bool`, optional
Whether to copy header from `ref_img` to new image. Default: True
Returns
-------
nii : :obj:`nibabel.nifti1.Nifti1Image`
NiftiImage
"""
ref_img = check_niimg(ref_img)
newdata = data.reshape(ref_img.shape[:3] + data.shape[1:])
if '.nii' not in ref_img.valid_exts:
# this is rather ugly and may lose some information...
nii = nib.Nifti1Image(newdata, affine=ref_img.affine,
header=ref_img.header)
else:
# nilearn's `new_img_like` is a very nice function
nii = new_img_like(ref_img, newdata, affine=affine,
copy_header=copy_header)
nii.set_data_dtype(data.dtype)
return nii
def filewrite(data, filename, ref_img, gzip=False, copy_header=True):
"""
Writes `data` to `filename` in format of `ref_img`
Parameters
----------
data : (S [x T]) array_like
Data to be saved
filename : :obj:`str`
Filepath where data should be saved to
ref_img : :obj:`str` or img_like
Reference image
gzip : :obj:`bool`, optional
Whether to gzip output (if not specified in `filename`). Only applies
if output dtype is NIFTI. Default: False
copy_header : :obj:`bool`, optional
Whether to copy header from `ref_img` to new image. Default: True
Returns
-------
name : :obj:`str`
Path of saved image (with added extensions, as appropriate)
"""
# get reference image for comparison
if isinstance(ref_img, list):
ref_img = ref_img[0]
# generate out file for saving
out = new_nii_like(ref_img, data, copy_header=copy_header)
# FIXME: we only handle writing to nifti right now
# get root of desired output file and save as nifti image
root, ext, add = splitext_addext(filename)
name = '{}.{}'.format(root, 'nii.gz' if gzip else 'nii')
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([utils.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 = utils.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, 1)), 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