Skip to content

Commit a2d0f57

Browse files
authored
Merge pull request #2522 from oesteban/enh/activations-count-map
ENH: Add an activation count map interface
2 parents a1ab518 + 728bc1b commit a2d0f57

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed

nipype/algorithms/stats.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
"""
5+
Managing statistical maps
6+
"""
7+
from __future__ import (print_function, division, unicode_literals,
8+
absolute_import)
9+
import os
10+
import nibabel as nb
11+
import numpy as np
12+
13+
from ..interfaces.base import (
14+
BaseInterfaceInputSpec, TraitedSpec, SimpleInterface,
15+
traits, InputMultiPath, File
16+
)
17+
from ..utils.filemanip import split_filename
18+
19+
20+
class ActivationCountInputSpec(BaseInterfaceInputSpec):
21+
in_files = InputMultiPath(File(exists=True), mandatory=True,
22+
desc='input file, generally a list of z-stat maps')
23+
threshold = traits.Float(
24+
mandatory=True, desc='binarization threshold. E.g. a threshold of 1.65 '
25+
'corresponds to a two-sided Z-test of p<.10')
26+
27+
28+
class ActivationCountOutputSpec(TraitedSpec):
29+
out_file = File(exists=True, desc='output activation count map')
30+
acm_pos = File(exists=True, desc='positive activation count map')
31+
acm_neg = File(exists=True, desc='negative activation count map')
32+
33+
34+
class ActivationCount(SimpleInterface):
35+
"""
36+
Calculate a simple Activation Count Maps
37+
38+
Adapted from: https://github.com/poldracklab/CNP_task_analysis/\
39+
blob/61c27f5992db9d8800884f8ffceb73e6957db8af/CNP_2nd_level_ACM.py
40+
"""
41+
input_spec = ActivationCountInputSpec
42+
output_spec = ActivationCountOutputSpec
43+
44+
def _run_interface(self, runtime):
45+
allmaps = nb.concat_images(self.inputs.in_files).get_data()
46+
acm_pos = np.mean(allmaps > self.inputs.threshold,
47+
axis=3, dtype=np.float32)
48+
acm_neg = np.mean(allmaps < -1.0 * self.inputs.threshold,
49+
axis=3, dtype=np.float32)
50+
acm_diff = acm_pos - acm_neg
51+
52+
template_fname = self.inputs.in_files[0]
53+
ext = split_filename(template_fname)[2]
54+
fname_fmt = os.path.join(runtime.cwd, 'acm_{}' + ext).format
55+
56+
self._results['out_file'] = fname_fmt('diff')
57+
self._results['acm_pos'] = fname_fmt('pos')
58+
self._results['acm_neg'] = fname_fmt('neg')
59+
60+
img = nb.load(template_fname)
61+
img.__class__(acm_diff, img.affine, img.header).to_filename(
62+
self._results['out_file'])
63+
img.__class__(acm_pos, img.affine, img.header).to_filename(
64+
self._results['acm_pos'])
65+
img.__class__(acm_neg, img.affine, img.header).to_filename(
66+
self._results['acm_neg'])
67+
68+
return runtime
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from __future__ import unicode_literals
3+
from ..stats import ActivationCount
4+
5+
6+
def test_ActivationCount_inputs():
7+
input_map = dict(
8+
ignore_exception=dict(
9+
deprecated='1.0.0',
10+
nohash=True,
11+
usedefault=True,
12+
),
13+
in_files=dict(mandatory=True, ),
14+
threshold=dict(mandatory=True, ),
15+
)
16+
inputs = ActivationCount.input_spec()
17+
18+
for key, metadata in list(input_map.items()):
19+
for metakey, value in list(metadata.items()):
20+
assert getattr(inputs.traits()[key], metakey) == value
21+
def test_ActivationCount_outputs():
22+
output_map = dict(
23+
acm_neg=dict(),
24+
acm_pos=dict(),
25+
out_file=dict(),
26+
)
27+
outputs = ActivationCount.output_spec()
28+
29+
for key, metadata in list(output_map.items()):
30+
for metakey, value in list(metadata.items()):
31+
assert getattr(outputs.traits()[key], metakey) == value

nipype/algorithms/tests/test_stats.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
5+
import numpy as np
6+
import nibabel as nb
7+
from nipype.algorithms.stats import ActivationCount
8+
import pytest
9+
10+
11+
def test_ActivationCount(tmpdir):
12+
tmpdir.chdir()
13+
in_files = ['{:d}.nii'.format(i) for i in range(3)]
14+
for fname in in_files:
15+
nb.Nifti1Image(np.random.normal(size=(5, 5, 5)),
16+
np.eye(4)).to_filename(fname)
17+
18+
acm = ActivationCount(in_files=in_files, threshold=1.65)
19+
res = acm.run()
20+
diff = nb.load(res.outputs.out_file)
21+
pos = nb.load(res.outputs.acm_pos)
22+
neg = nb.load(res.outputs.acm_neg)
23+
assert np.allclose(diff.get_data(), pos.get_data() - neg.get_data())
24+
25+
26+
@pytest.mark.parametrize("threshold, above_thresh", [
27+
(1, 15.865), # above one standard deviation (one side)
28+
(2, 2.275), # above two standard deviations (one side)
29+
(3, 0.135) # above three standard deviations (one side)
30+
])
31+
def test_ActivationCount_normaldistr(tmpdir, threshold, above_thresh):
32+
tmpdir.chdir()
33+
in_files = ['{:d}.nii'.format(i) for i in range(3)]
34+
for fname in in_files:
35+
nb.Nifti1Image(np.random.normal(size=(100, 100, 100)),
36+
np.eye(4)).to_filename(fname)
37+
38+
acm = ActivationCount(in_files=in_files, threshold=threshold)
39+
res = acm.run()
40+
pos = nb.load(res.outputs.acm_pos)
41+
neg = nb.load(res.outputs.acm_neg)
42+
assert np.isclose(pos.get_data().mean(),
43+
above_thresh * 1.e-2, rtol=0.1, atol=1.e-4)
44+
assert np.isclose(neg.get_data().mean(),
45+
above_thresh * 1.e-2, rtol=0.1, atol=1.e-4)

0 commit comments

Comments
 (0)