-
Notifications
You must be signed in to change notification settings - Fork 535
ENH: Add an activation count map interface #2522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
56fbac3
f6a9a81
756088a
4cf7f66
81167ca
8a294e0
068dda0
5b9fc04
beaf3b8
4b93c9e
60e97d5
608c4d2
f81c1cf
b126b70
6e53f01
6348862
9e07a6e
d3d75f7
e0ba996
02b7dfd
cfb19c4
31bc311
728bc1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# -*- coding: utf-8 -*- | ||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
""" | ||
Managing statistical maps | ||
""" | ||
from __future__ import (print_function, division, unicode_literals, | ||
absolute_import) | ||
import os | ||
import nibabel as nb | ||
import numpy as np | ||
|
||
from ..interfaces.base import ( | ||
BaseInterfaceInputSpec, TraitedSpec, SimpleInterface, | ||
traits, InputMultiPath, File | ||
) | ||
from ..utils.filemanip import split_filename | ||
|
||
|
||
class ActivationCountInputSpec(BaseInterfaceInputSpec): | ||
in_files = InputMultiPath(File(exists=True), mandatory=True, | ||
desc='input file, generally a list of z-stat maps') | ||
threshold = traits.Float(1.65, usedefault=True, | ||
desc='binarization threshold. A z-value of 1.65 ' | ||
'corresponds to a two-sided test of p<.10') | ||
|
||
|
||
class ActivationCountOutputSpec(TraitedSpec): | ||
out_file = File(exists=True, desc='output activation count map') | ||
acm_pos = File(exists=True, desc='positive activation count map') | ||
acm_neg = File(exists=True, desc='negative activation count map') | ||
|
||
|
||
class ActivationCount(SimpleInterface): | ||
""" | ||
Calculate a simple Activation Count Maps | ||
|
||
Adapted from: https://github.com/poldracklab/CNP_task_analysis/\ | ||
blob/61c27f5992db9d8800884f8ffceb73e6957db8af/CNP_2nd_level_ACM.py | ||
""" | ||
input_spec = ActivationCountInputSpec | ||
output_spec = ActivationCountOutputSpec | ||
|
||
def _run_interface(self, runtime): | ||
allmaps = nb.concat_images(self.inputs.in_files).get_data() | ||
acm_pos = np.mean(allmaps > self.inputs.threshold, | ||
axis=3, dtype=np.float32) | ||
acm_neg = np.mean(allmaps < -1.0 * self.inputs.threshold, | ||
axis=3, dtype=np.float32) | ||
acm_diff = acm_pos - acm_neg | ||
|
||
template_fname = self.inputs.in_files[0] | ||
ext = split_filename(template_fname)[2] | ||
fname_fmt = os.path.join(runtime.cwd, 'acm_{}' + ext).format | ||
|
||
self._results['out_file'] = fname_fmt('diff') | ||
self._results['acm_pos'] = fname_fmt('pos') | ||
self._results['acm_neg'] = fname_fmt('neg') | ||
|
||
img = nb.load(template_fname) | ||
img.__class__(acm_diff, img.affine, img.header).to_filename( | ||
self._results['out_file']) | ||
img.__class__(acm_pos, img.affine, img.header).to_filename( | ||
self._results['acm_pos']) | ||
img.__class__(acm_neg, img.affine, img.header).to_filename( | ||
self._results['acm_neg']) | ||
|
||
return runtime |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT | ||
from __future__ import unicode_literals | ||
from ..stats import ActivationCount | ||
|
||
|
||
def test_ActivationCount_inputs(): | ||
input_map = dict( | ||
ignore_exception=dict( | ||
deprecated='1.0.0', | ||
nohash=True, | ||
usedefault=True, | ||
), | ||
in_files=dict(mandatory=True, ), | ||
threshold=dict(usedefault=True, ), | ||
) | ||
inputs = ActivationCount.input_spec() | ||
|
||
for key, metadata in list(input_map.items()): | ||
for metakey, value in list(metadata.items()): | ||
assert getattr(inputs.traits()[key], metakey) == value | ||
def test_ActivationCount_outputs(): | ||
output_map = dict( | ||
acm_neg=dict(), | ||
acm_pos=dict(), | ||
out_file=dict(), | ||
) | ||
outputs = ActivationCount.output_spec() | ||
|
||
for key, metadata in list(output_map.items()): | ||
for metakey, value in list(metadata.items()): | ||
assert getattr(outputs.traits()[key], metakey) == value |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# -*- coding: utf-8 -*- | ||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
|
||
import numpy as np | ||
import nibabel as nb | ||
from nipype.algorithms.stats import ActivationCount | ||
|
||
|
||
def test_ActivationCount(tmpdir): | ||
tmpdir.chdir() | ||
in_files = ['{:d}.nii'.format(i) for i in range(3)] | ||
for fname in in_files: | ||
nb.Nifti1Image(np.random.normal(size=(5, 5, 5)), | ||
np.eye(4)).to_filename(fname) | ||
|
||
acm = ActivationCount(in_files=in_files) | ||
res = acm.run() | ||
diff = nb.load(res.outputs.out_file) | ||
pos = nb.load(res.outputs.acm_pos) | ||
neg = nb.load(res.outputs.acm_neg) | ||
assert np.allclose(diff.get_data(), pos.get_data() - neg.get_data()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could also check some statistics There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main things I can think of that should be true of performing this on some random noise would be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand correctly, the interface just checks how many point are above There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here is an example: oesteban@6e53f01 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That looks valid, but I'm not fully clear on what assurances this gets us for the interface. I guess, roughly, that Gaussian noise is being filtered as we'd expect? I'm okay if this test goes in, but my overall feeling is that a useful test asserts properties that should be true given any real or simulated inputs, and this seems tied to the specific distribution of simulated random variates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you usually use specific cases for tests, and often some simple cases, so you know the answer. The interface should work for any real or simulated inputs, but I'm checking here for a specific distribution (exactly the same as @oesteban used in the original test). Obviously I'm not able to predict answers for completely random set of numbers, but for the normal distribution I can. I just thought that this adds extra checks to the original @oesteban test, that was only checking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can also just generate a small array and calculate the expected output of the interface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if it's better to leave
threshold
without default value, so users have to really choose one....There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have an opinion here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I'll update accordingly.