Skip to content

Commit d73ba9d

Browse files
authored
Merge pull request #1170 from effigies/rf/signal_extract_lowmem
RF: Reimplement SignalExtraction
2 parents f0f76ce + 7877d16 commit d73ba9d

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

fmriprep/interfaces/images.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,77 @@ def nii_ones_like(in_file, value, dtype, newpath=None):
572572
nii.to_filename(out_file)
573573

574574
return out_file
575+
576+
577+
class SignalExtractionInputSpec(BaseInterfaceInputSpec):
578+
in_file = File(exists=True, mandatory=True, desc='4-D fMRI nii file')
579+
label_files = InputMultiPath(
580+
File(exists=True),
581+
mandatory=True,
582+
desc='a 3-D label image, with 0 denoting '
583+
'background, or a list of 3-D probability '
584+
'maps (one per label) or the equivalent 4D '
585+
'file.')
586+
class_labels = traits.List(
587+
mandatory=True,
588+
desc='Human-readable labels for each segment '
589+
'in the label file, in order. The length of '
590+
'class_labels must be equal to the number of '
591+
'segments (background excluded). This list '
592+
'corresponds to the class labels in label_file '
593+
'in ascending order')
594+
out_file = File(
595+
'signals.tsv',
596+
usedefault=True,
597+
exists=False,
598+
desc='The name of the file to output to. '
599+
'signals.tsv by default')
600+
601+
602+
class SignalExtractionOutputSpec(TraitedSpec):
603+
out_file = File(
604+
exists=True,
605+
desc='tsv file containing the computed '
606+
'signals, with as many columns as there are labels and as '
607+
'many rows as there are timepoints in in_file, plus a '
608+
'header row with values from class_labels')
609+
610+
611+
class SignalExtraction(SimpleInterface):
612+
""" Extract mean signals from a time series within a set of ROIs
613+
614+
This interface is intended to be a memory-efficient alternative to
615+
nipype.interfaces.nilearn.SignalExtraction.
616+
Not all features of nilearn.SignalExtraction are implemented at
617+
this time.
618+
"""
619+
input_spec = SignalExtractionInputSpec
620+
output_spec = SignalExtractionOutputSpec
621+
622+
def _run_interface(self, runtime):
623+
mask_imgs = [nb.load(fname) for fname in self.inputs.label_files]
624+
if len(mask_imgs) == 1:
625+
mask_imgs = nb.four_to_three(mask_imgs[0])
626+
627+
masks = [mask_img.get_data().astype(np.bool) for mask_img in mask_imgs]
628+
629+
n_masks = len(masks)
630+
631+
if n_masks != len(self.inputs.class_labels):
632+
raise ValueError("Number of masks must match number of labels")
633+
634+
img = nb.load(self.inputs.in_file)
635+
636+
series = np.zeros((img.shape[3], n_masks))
637+
638+
data = img.get_data()
639+
for j in range(n_masks):
640+
series[:, j] = data[masks[j], :].mean(axis=0)
641+
642+
output = np.vstack((self.inputs.class_labels, series.astype(str)))
643+
self._results['out_file'] = os.path.join(runtime.cwd,
644+
self.inputs.out_file)
645+
np.savetxt(
646+
self._results['out_file'], output, fmt=b'%s', delimiter='\t')
647+
648+
return runtime

fmriprep/interfaces/tests/__init__.py

Whitespace-only changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import time
2+
import numpy as np
3+
import nibabel as nb
4+
from nipype.interfaces import nilearn as nl
5+
from .. import images as im
6+
7+
import pytest
8+
9+
10+
@pytest.mark.parametrize('nvols, nmasks, ext, factor', [
11+
(500, 10, '.nii', 2),
12+
(500, 10, '.nii.gz', 5),
13+
(200, 3, '.nii', 1.1),
14+
(200, 3, '.nii.gz', 2),
15+
(200, 10, '.nii', 1.1),
16+
(200, 10, '.nii.gz', 2),
17+
])
18+
def test_signal_extraction_equivalence(tmpdir, nvols, nmasks, ext, factor):
19+
tmpdir.chdir()
20+
21+
vol_shape = (64, 64, 40)
22+
23+
img_fname = 'img' + ext
24+
masks_fname = 'masks' + ext
25+
26+
random_data = np.random.random(size=vol_shape + (nvols,)) * 2000
27+
random_mask_data = np.random.random(size=vol_shape + (nmasks,)) < 0.2
28+
29+
nb.Nifti1Image(random_data, np.eye(4)).to_filename(img_fname)
30+
nb.Nifti1Image(random_mask_data.astype(np.uint8), np.eye(4)).to_filename(masks_fname)
31+
32+
se1 = nl.SignalExtraction(in_file=img_fname, label_files=masks_fname,
33+
class_labels=['a%d' % i for i in range(nmasks)],
34+
out_file='nlsignals.tsv')
35+
se2 = im.SignalExtraction(in_file=img_fname, label_files=masks_fname,
36+
class_labels=['a%d' % i for i in range(nmasks)],
37+
out_file='imsignals.tsv')
38+
39+
tic = time.time()
40+
se1.run()
41+
toc = time.time()
42+
se2.run()
43+
toc2 = time.time()
44+
45+
tab1 = np.loadtxt('nlsignals.tsv', skiprows=1)
46+
tab2 = np.loadtxt('imsignals.tsv', skiprows=1)
47+
48+
assert np.allclose(tab1, tab2)
49+
50+
t1 = toc - tic
51+
t2 = toc2 - toc
52+
53+
assert t2 < t1 / factor

0 commit comments

Comments
 (0)