Skip to content

Commit 1dd4995

Browse files
authored
Merge pull request #2167 from mgxd/enh/calcmedian
enh: calcmedian interface
2 parents a0f12e7 + 7055313 commit 1dd4995

File tree

6 files changed

+139
-47
lines changed

6 files changed

+139
-47
lines changed

examples/fmri_ants_openfmri.py

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import nipype.pipeline.engine as pe
2727
import nipype.algorithms.modelgen as model
2828
import nipype.algorithms.rapidart as ra
29-
from nipype.algorithms.misc import TSNR
29+
from nipype.algorithms.misc import TSNR, CalculateMedian
3030
from nipype.interfaces.c3 import C3dAffineTool
3131
from nipype.interfaces import fsl, Function, ants, freesurfer as fs
3232
import nipype.interfaces.io as nio
@@ -55,33 +55,6 @@
5555
'from scipy.special import legendre'
5656
]
5757

58-
def median(in_files):
59-
"""Computes an average of the median of each realigned timeseries
60-
61-
Parameters
62-
----------
63-
64-
in_files: one or more realigned Nifti 4D time series
65-
66-
Returns
67-
-------
68-
69-
out_file: a 3D Nifti file
70-
"""
71-
average = None
72-
for idx, filename in enumerate(filename_to_list(in_files)):
73-
img = nb.load(filename, mmap=NUMPY_MMAP)
74-
data = np.median(img.get_data(), axis=3)
75-
if average is None:
76-
average = data
77-
else:
78-
average = average + data
79-
median_img = nb.Nifti1Image(average / float(idx + 1), img.affine,
80-
img.header)
81-
filename = os.path.join(os.getcwd(), 'median.nii.gz')
82-
median_img.to_filename(filename)
83-
return filename
84-
8558

8659
def create_reg_workflow(name='registration'):
8760
"""Create a FEAT preprocessing workflow together with freesurfer
@@ -818,11 +791,7 @@ def check_behav_list(behav, run_id, conds):
818791
wf.connect(preproc, "outputspec.realigned_files", tsnr, "in_file")
819792

820793
# Compute the median image across runs
821-
calc_median = Node(Function(input_names=['in_files'],
822-
output_names=['median_file'],
823-
function=median,
824-
imports=imports),
825-
name='median')
794+
calc_median = Node(CalculateMedian(), name='median')
826795
wf.connect(tsnr, 'detrended_file', calc_median, 'in_files')
827796

828797
"""

examples/rsfmri_vol_surface_preprocessing.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
# mlab.MatlabCommand.set_default_paths('/software/matlab/spm12')
6868

6969
from nipype.algorithms.rapidart import ArtifactDetect
70-
from nipype.algorithms.misc import TSNR
70+
from nipype.algorithms.misc import TSNR, CalculateMedian
7171
from nipype.interfaces.utility import Rename, Merge, IdentityInterface
7272
from nipype.utils.filemanip import filename_to_list
7373
from nipype.interfaces.io import DataSink, FreeSurferSource
@@ -623,11 +623,7 @@ def create_workflow(files,
623623
wf.connect(slice_timing, 'timecorrected_files', tsnr, 'in_file')
624624

625625
# Compute the median image across runs
626-
calc_median = Node(Function(input_names=['in_files'],
627-
output_names=['median_file'],
628-
function=median,
629-
imports=imports),
630-
name='median')
626+
calc_median = Node(CalculateMedian(), name='median')
631627
wf.connect(tsnr, 'detrended_file', calc_median, 'in_files')
632628

633629
"""Segment and Register

examples/rsfmri_vol_surface_preprocessing_nipy.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
from nipype import Workflow, Node, MapNode
6666

6767
from nipype.algorithms.rapidart import ArtifactDetect
68-
from nipype.algorithms.misc import TSNR
68+
from nipype.algorithms.misc import TSNR, CalculateMedian
6969
from nipype.algorithms.confounds import ACompCor
7070
from nipype.interfaces.utility import Rename, Merge, IdentityInterface
7171
from nipype.utils.filemanip import filename_to_list
@@ -556,11 +556,7 @@ def create_workflow(files,
556556
wf.connect(realign, "out_file", tsnr, "in_file")
557557

558558
# Compute the median image across runs
559-
calc_median = Node(Function(input_names=['in_files'],
560-
output_names=['median_file'],
561-
function=median,
562-
imports=imports),
563-
name='median')
559+
calc_median = Node(CalculateMedian(), name='median')
564560
wf.connect(tsnr, 'detrended_file', calc_median, 'in_files')
565561

566562
"""Segment and Register

nipype/algorithms/misc.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
InputMultiPath, OutputMultiPath,
3434
BaseInterfaceInputSpec, isdefined,
3535
DynamicTraitedSpec, Undefined)
36-
from ..utils.filemanip import fname_presuffix, split_filename
36+
from ..utils.filemanip import fname_presuffix, split_filename, filename_to_list
3737
from ..utils import NUMPY_MMAP
3838

3939
from . import confounds
@@ -1380,6 +1380,94 @@ def merge_rois(in_files, in_idxs, in_ref,
13801380
return out_file
13811381

13821382

1383+
class CalculateMedianInputSpec(BaseInterfaceInputSpec):
1384+
in_files = InputMultiPath(File(exists=True, mandatory=True,
1385+
desc="One or more realigned Nifti 4D timeseries"))
1386+
median_file = traits.Str(desc="Filename prefix to store median images")
1387+
median_per_file = traits.Bool(False, usedefault=True,
1388+
desc="Calculate a median file for each Nifti")
1389+
1390+
class CalculateMedianOutputSpec(TraitedSpec):
1391+
median_files = OutputMultiPath(File(exists=True),
1392+
desc="One or more median images")
1393+
1394+
class CalculateMedian(BaseInterface):
1395+
"""
1396+
Computes an average of the median across one or more 4D Nifti timeseries
1397+
1398+
Example
1399+
-------
1400+
1401+
>>> from nipype.algorithms.misc import CalculateMedian
1402+
>>> mean = CalculateMedian()
1403+
>>> mean.inputs.in_files = 'functional.nii'
1404+
>>> mean.run() # doctest: +SKIP
1405+
1406+
"""
1407+
input_spec = CalculateMedianInputSpec
1408+
output_spec = CalculateMedianOutputSpec
1409+
1410+
def __init__(self, *args, **kwargs):
1411+
super(CalculateMedian, self).__init__(*args, **kwargs)
1412+
self._median_files = []
1413+
1414+
def _gen_fname(self, suffix, idx=None, ext=None):
1415+
if idx:
1416+
in_file = self.inputs.in_files[idx]
1417+
else:
1418+
if isinstance(self.inputs.in_files, list):
1419+
in_file = self.inputs.in_files[0]
1420+
else:
1421+
in_file = self.inputs.in_files
1422+
fname, in_ext = op.splitext(op.basename(in_file))
1423+
if in_ext == '.gz':
1424+
fname, in_ext2 = op.splitext(fname)
1425+
in_ext = in_ext2 + in_ext
1426+
if ext is None:
1427+
ext = in_ext
1428+
if ext.startswith('.'):
1429+
ext = ext[1:]
1430+
if self.inputs.median_file:
1431+
outname = self.inputs.median_file
1432+
else:
1433+
outname = '{}_{}'.format(fname, suffix)
1434+
if idx:
1435+
outname += str(idx)
1436+
return op.abspath('{}.{}'.format(outname, ext))
1437+
1438+
def _run_interface(self, runtime):
1439+
total = None
1440+
self._median_files = []
1441+
for idx, fname in enumerate(filename_to_list(self.inputs.in_files)):
1442+
img = nb.load(fname, mmap=NUMPY_MMAP)
1443+
data = np.median(img.get_data(), axis=3)
1444+
if self.inputs.median_per_file:
1445+
self._median_files.append(self._write_nifti(img, data, idx))
1446+
else:
1447+
if total is None:
1448+
total = data
1449+
else:
1450+
total += data
1451+
if not self.inputs.median_per_file:
1452+
self._median_files.append(self._write_nifti(img, total, idx))
1453+
return runtime
1454+
1455+
def _list_outputs(self):
1456+
outputs = self._outputs().get()
1457+
outputs['median_files'] = self._median_files
1458+
return outputs
1459+
1460+
def _write_nifti(self, img, data, idx, suffix='median'):
1461+
if self.inputs.median_per_file:
1462+
median_img = nb.Nifti1Image(data, img.affine, img.header)
1463+
filename = self._gen_fname(suffix, idx=idx)
1464+
else:
1465+
median_img = nb.Nifti1Image(data/(idx+1), img.affine, img.header)
1466+
filename = self._gen_fname(suffix)
1467+
median_img.to_filename(filename)
1468+
return filename
1469+
1470+
13831471
# Deprecated interfaces ------------------------------------------------------
13841472

13851473
class Distance(nam.Distance):
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from __future__ import unicode_literals
3+
from ..misc import CalculateMedian
4+
5+
6+
def test_CalculateMedian_inputs():
7+
input_map = dict(ignore_exception=dict(nohash=True,
8+
usedefault=True,
9+
),
10+
in_files=dict(),
11+
median_file=dict(),
12+
median_per_file=dict(usedefault=True,
13+
),
14+
)
15+
inputs = CalculateMedian.input_spec()
16+
17+
for key, metadata in list(input_map.items()):
18+
for metakey, value in list(metadata.items()):
19+
assert getattr(inputs.traits()[key], metakey) == value
20+
21+
22+
def test_CalculateMedian_outputs():
23+
output_map = dict(median_files=dict(),
24+
)
25+
outputs = CalculateMedian.output_spec()
26+
27+
for key, metadata in list(output_map.items()):
28+
for metakey, value in list(metadata.items()):
29+
assert getattr(outputs.traits()[key], metakey) == value

nipype/algorithms/tests/test_misc.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from nipype.algorithms import misc
1010
from nipype.utils.filemanip import fname_presuffix
1111
from nipype.testing.fixtures import create_analyze_pair_file_in_directory
12+
from nipype.utils import NUMPY_MMAP
13+
from nipype.testing import example_data
1214

1315

1416
def test_CreateNifti(create_analyze_pair_file_in_directory):
@@ -31,4 +33,16 @@ def test_CreateNifti(create_analyze_pair_file_in_directory):
3133
result = create_nifti.run()
3234

3335
assert os.path.exists(result.outputs.nifti_file)
34-
assert nb.load(result.outputs.nifti_file)
36+
assert nb.load(result.outputs.nifti_file, mmap=NUMPY_MMAP)
37+
38+
def test_CalculateMedian(create_analyze_pair_file_in_directory):
39+
40+
mean = misc.CalculateMedian()
41+
42+
with pytest.raises(TypeError): mean.run()
43+
44+
mean.inputs.in_files = example_data('ds003_sub-01_mc.nii.gz')
45+
eg = mean.run()
46+
47+
assert os.path.exists(eg.outputs.median_files)
48+
assert nb.load(eg.outputs.median_files, mmap=NUMPY_MMAP)

0 commit comments

Comments
 (0)