Skip to content

Commit ccbbe56

Browse files
committed
Merge pull request #1361 from oesteban/enh/AddAfniFWHM
[ENH] Added afni.FWHMx interface
2 parents e2ec11c + 2215ee5 commit ccbbe56

File tree

6 files changed

+346
-17
lines changed

6 files changed

+346
-17
lines changed

CHANGES

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
Next release
22
============
33

4-
* ENH: Added interfaces of AFNI (https://github.com/nipy/nipype/pull/1360)
4+
* ENH: Added interfaces of AFNI (https://github.com/nipy/nipype/pull/1360,
5+
https://github.com/nipy/nipype/pull/1361)
56
* ENH: Added support for PETPVC (https://github.com/nipy/nipype/pull/1335)
67
* ENH: Merge S3DataSink into DataSink, added AWS documentation (https://github.com/nipy/nipype/pull/1316)
78
* TST: Cache APT in CircleCI (https://github.com/nipy/nipype/pull/1333)

nipype/interfaces/afni/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
Fourier, Allineate, Maskave, SkullStrip, TCat, Fim,
1313
BlurInMask, Autobox, TCorrMap, Bandpass, Retroicor,
1414
TCorrelate, TCorr1D, BrickStat, ROIStats, AutoTcorrelate,
15-
AFNItoNIFTI, Eval, Means, Hist)
15+
AFNItoNIFTI, Eval, Means, Hist, FWHMx)
1616
from .svm import (SVMTest, SVMTrain)

nipype/interfaces/afni/base.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
"""Provide interface to AFNI commands."""
44

5-
from builtins import object
6-
7-
85
import os
6+
from sys import platform
7+
from builtins import object
98

109
from ... import logging
1110
from ...utils.filemanip import split_filename
1211
from ..base import (
1312
CommandLine, traits, CommandLineInputSpec, isdefined, File, TraitedSpec)
1413

1514
# Use nipype's logging system
16-
iflogger = logging.getLogger('interface')
15+
IFLOGGER = logging.getLogger('interface')
1716

1817

1918
class Info(object):
@@ -46,14 +45,14 @@ def version():
4645
currv = clout.runtime.stdout.split('\n')[1].split('=', 1)[1].strip()
4746
except IOError:
4847
# If afni_vcheck is not present, return None
49-
iflogger.warn('afni_vcheck executable not found.')
48+
IFLOGGER.warn('afni_vcheck executable not found.')
5049
return None
5150
except RuntimeError as e:
5251
# If AFNI is outdated, afni_vcheck throws error.
5352
# Show new version, but parse current anyways.
5453
currv = str(e).split('\n')[4].split('=', 1)[1].strip()
5554
nextv = str(e).split('\n')[6].split('=', 1)[1].strip()
56-
iflogger.warn(
55+
IFLOGGER.warn(
5756
'AFNI is outdated, detected version %s and %s is available.' % (currv, nextv))
5857

5958
if currv.startswith('AFNI_'):
@@ -117,6 +116,17 @@ def standard_image(img_name):
117116
return os.path.join(basedir, img_name)
118117

119118

119+
class AFNICommandBase(CommandLine):
120+
"""
121+
A base class to fix a linking problem in OSX and afni.
122+
See http://afni.nimh.nih.gov/afni/community/board/read.php?1,145346,145347#msg-145347
123+
"""
124+
def _run_interface(self, runtime):
125+
if platform == 'darwin':
126+
runtime.environ['DYLD_FALLBACK_LIBRARY_PATH'] = '/usr/local/afni/'
127+
return super(AFNICommandBase, self)._run_interface(runtime)
128+
129+
120130
class AFNICommandInputSpec(CommandLineInputSpec):
121131
outputtype = traits.Enum('AFNI', list(Info.ftypes.keys()),
122132
desc='AFNI output filetype')
@@ -130,8 +140,8 @@ class AFNICommandOutputSpec(TraitedSpec):
130140
exists=True)
131141

132142

133-
class AFNICommand(CommandLine):
134-
143+
class AFNICommand(AFNICommandBase):
144+
"""Shared options for several AFNI commands """
135145
input_spec = AFNICommandInputSpec
136146
_outputtype = None
137147

nipype/interfaces/afni/preprocess.py

Lines changed: 219 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
import os
1313
import os.path as op
1414
import re
15-
from warnings import warn
15+
import numpy as np
1616

17-
from .base import AFNICommand, AFNICommandInputSpec, AFNICommandOutputSpec, Info, no_afni
18-
from ..base import CommandLineInputSpec, CommandLine, OutputMultiPath
17+
from .base import (AFNICommandBase, AFNICommand, AFNICommandInputSpec, AFNICommandOutputSpec,
18+
Info, no_afni)
19+
from ..base import CommandLineInputSpec
1920
from ..base import (Directory, TraitedSpec,
2021
traits, isdefined, File, InputMultiPath, Undefined)
22+
from ...external.six import string_types
2123
from ...utils.filemanip import (load_json, save_json, split_filename)
22-
from ...utils.filemanip import fname_presuffix
24+
2325

2426
class To3DInputSpec(AFNICommandInputSpec):
2527
out_file = File(name_template="%s", desc='output image file name',
@@ -180,7 +182,7 @@ class RefitInputSpec(CommandLineInputSpec):
180182
' template type, e.g. TLRC, MNI, ORIG')
181183

182184

183-
class Refit(CommandLine):
185+
class Refit(AFNICommandBase):
184186
"""Changes some of the information inside a 3D dataset's header
185187
186188
For complete details, see the `3drefit Documentation.
@@ -1544,7 +1546,7 @@ class ROIStatsOutputSpec(TraitedSpec):
15441546
stats = File(desc='output tab separated values file', exists=True)
15451547

15461548

1547-
class ROIStats(CommandLine):
1549+
class ROIStats(AFNICommandBase):
15481550
"""Display statistics over masked regions
15491551
15501552
For complete details, see the `3dROIstats Documentation.
@@ -2113,7 +2115,7 @@ class HistOutputSpec(TraitedSpec):
21132115
out_show = File(desc='output visual histogram')
21142116

21152117

2116-
class Hist(CommandLine):
2118+
class Hist(AFNICommandBase):
21172119
"""Computes average of all voxels in the input dataset
21182120
which satisfy the criterion in the options list
21192121
@@ -2160,3 +2162,213 @@ def _list_outputs(self):
21602162
if not self.inputs.showhist:
21612163
outputs['out_show'] = Undefined
21622164
return outputs
2165+
2166+
2167+
class FWHMxInputSpec(CommandLineInputSpec):
2168+
in_file = File(desc='input dataset', argstr='-input %s', mandatory=True, exists=True)
2169+
out_file = File(argstr='> %s', name_source='in_file', name_template='%s_fwhmx.out',
2170+
position=-1, keep_extension=False, desc='output file')
2171+
out_subbricks = File(argstr='-out %s', name_source='in_file', name_template='%s_subbricks.out',
2172+
keep_extension=False, desc='output file listing the subbricks FWHM')
2173+
mask = File(desc='use only voxels that are nonzero in mask', argstr='-mask %s', exists=True)
2174+
automask = traits.Bool(False, usedefault=True, argstr='-automask',
2175+
desc='compute a mask from THIS dataset, a la 3dAutomask')
2176+
detrend = traits.Either(
2177+
traits.Bool(), traits.Int(), default=False, argstr='-detrend', xor=['demed'], usedefault=True,
2178+
desc='instead of demed (0th order detrending), detrend to the specified order. If order '
2179+
'is not given, the program picks q=NT/30. -detrend disables -demed, and includes '
2180+
'-unif.')
2181+
demed = traits.Bool(
2182+
False, argstr='-demed', xorg=['detrend'],
2183+
desc='If the input dataset has more than one sub-brick (e.g., has a time axis), then '
2184+
'subtract the median of each voxel\'s time series before processing FWHM. This will '
2185+
'tend to remove intrinsic spatial structure and leave behind the noise.')
2186+
unif = traits.Bool(False, argstr='-unif',
2187+
desc='If the input dataset has more than one sub-brick, then normalize each'
2188+
' voxel\'s time series to have the same MAD before processing FWHM.')
2189+
out_detrend = File(argstr='-detprefix %s', name_source='in_file', name_template='%s_detrend',
2190+
keep_extension=False, desc='Save the detrended file into a dataset')
2191+
geom = traits.Bool(argstr='-geom', xor=['arith'],
2192+
desc='if in_file has more than one sub-brick, compute the final estimate as'
2193+
'the geometric mean of the individual sub-brick FWHM estimates')
2194+
arith = traits.Bool(argstr='-arith', xor=['geom'],
2195+
desc='if in_file has more than one sub-brick, compute the final estimate as'
2196+
'the arithmetic mean of the individual sub-brick FWHM estimates')
2197+
combine = traits.Bool(argstr='-combine', desc='combine the final measurements along each axis')
2198+
compat = traits.Bool(argstr='-compat', desc='be compatible with the older 3dFWHM')
2199+
acf = traits.Either(
2200+
traits.Bool(), File(), traits.Tuple(File(exists=True), traits.Float()),
2201+
default=False, usedefault=True, argstr='-acf', desc='computes the spatial autocorrelation')
2202+
2203+
2204+
class FWHMxOutputSpec(TraitedSpec):
2205+
out_file = File(exists=True, desc='output file')
2206+
out_subbricks = File(exists=True, desc='output file (subbricks)')
2207+
out_detrend = File(desc='output file, detrended')
2208+
fwhm = traits.Either(
2209+
traits.Tuple(traits.Float(), traits.Float(), traits.Float()),
2210+
traits.Tuple(traits.Float(), traits.Float(), traits.Float(), traits.Float()),
2211+
desc='FWHM along each axis')
2212+
acf_param = traits.Either(
2213+
traits.Tuple(traits.Float(), traits.Float(), traits.Float()),
2214+
traits.Tuple(traits.Float(), traits.Float(), traits.Float(), traits.Float()),
2215+
desc='fitted ACF model parameters')
2216+
out_acf = File(exists=True, desc='output acf file')
2217+
2218+
2219+
class FWHMx(AFNICommandBase):
2220+
"""
2221+
Unlike the older 3dFWHM, this program computes FWHMs for all sub-bricks
2222+
in the input dataset, each one separately. The output for each one is
2223+
written to the file specified by '-out'. The mean (arithmetic or geometric)
2224+
of all the FWHMs along each axis is written to stdout. (A non-positive
2225+
output value indicates something bad happened; e.g., FWHM in z is meaningless
2226+
for a 2D dataset; the estimation method computed incoherent intermediate results.)
2227+
2228+
Examples
2229+
--------
2230+
2231+
>>> from nipype.interfaces import afni as afp
2232+
>>> fwhm = afp.FWHMx()
2233+
>>> fwhm.inputs.in_file = 'functional.nii'
2234+
>>> fwhm.cmdline
2235+
'3dFWHMx -input functional.nii -out functional_subbricks.out > functional_fwhmx.out'
2236+
2237+
2238+
(Classic) METHOD:
2239+
2240+
* Calculate ratio of variance of first differences to data variance.
2241+
* Should be the same as 3dFWHM for a 1-brick dataset.
2242+
(But the output format is simpler to use in a script.)
2243+
2244+
2245+
.. note:: IMPORTANT NOTE [AFNI > 16]
2246+
2247+
A completely new method for estimating and using noise smoothness values is
2248+
now available in 3dFWHMx and 3dClustSim. This method is implemented in the
2249+
'-acf' options to both programs. 'ACF' stands for (spatial) AutoCorrelation
2250+
Function, and it is estimated by calculating moments of differences out to
2251+
a larger radius than before.
2252+
2253+
Notably, real FMRI data does not actually have a Gaussian-shaped ACF, so the
2254+
estimated ACF is then fit (in 3dFWHMx) to a mixed model (Gaussian plus
2255+
mono-exponential) of the form
2256+
2257+
.. math::
2258+
2259+
ACF(r) = a * exp(-r*r/(2*b*b)) + (1-a)*exp(-r/c)
2260+
2261+
2262+
where :math:`r` is the radius, and :math:`a, b, c` are the fitted parameters.
2263+
The apparent FWHM from this model is usually somewhat larger in real data
2264+
than the FWHM estimated from just the nearest-neighbor differences used
2265+
in the 'classic' analysis.
2266+
2267+
The longer tails provided by the mono-exponential are also significant.
2268+
3dClustSim has also been modified to use the ACF model given above to generate
2269+
noise random fields.
2270+
2271+
2272+
.. note:: TL;DR or summary
2273+
2274+
The take-awaymessage is that the 'classic' 3dFWHMx and
2275+
3dClustSim analysis, using a pure Gaussian ACF, is not very correct for
2276+
FMRI data -- I cannot speak for PET or MEG data.
2277+
2278+
2279+
.. warning::
2280+
2281+
Do NOT use 3dFWHMx on the statistical results (e.g., '-bucket') from
2282+
3dDeconvolve or 3dREMLfit!!! The function of 3dFWHMx is to estimate
2283+
the smoothness of the time series NOISE, not of the statistics. This
2284+
proscription is especially true if you plan to use 3dClustSim next!!
2285+
2286+
2287+
.. note:: Recommendations
2288+
2289+
* For FMRI statistical purposes, you DO NOT want the FWHM to reflect
2290+
the spatial structure of the underlying anatomy. Rather, you want
2291+
the FWHM to reflect the spatial structure of the noise. This means
2292+
that the input dataset should not have anatomical (spatial) structure.
2293+
* One good form of input is the output of '3dDeconvolve -errts', which is
2294+
the dataset of residuals left over after the GLM fitted signal model is
2295+
subtracted out from each voxel's time series.
2296+
* If you don't want to go to that much trouble, use '-detrend' to approximately
2297+
subtract out the anatomical spatial structure, OR use the output of 3dDetrend
2298+
for the same purpose.
2299+
* If you do not use '-detrend', the program attempts to find non-zero spatial
2300+
structure in the input, and will print a warning message if it is detected.
2301+
2302+
2303+
.. note:: Notes on -demend
2304+
2305+
* I recommend this option, and it is not the default only for historical
2306+
compatibility reasons. It may become the default someday.
2307+
* It is already the default in program 3dBlurToFWHM. This is the same detrending
2308+
as done in 3dDespike; using 2*q+3 basis functions for q > 0.
2309+
* If you don't use '-detrend', the program now [Aug 2010] checks if a large number
2310+
of voxels are have significant nonzero means. If so, the program will print a
2311+
warning message suggesting the use of '-detrend', since inherent spatial
2312+
structure in the image will bias the estimation of the FWHM of the image time
2313+
series NOISE (which is usually the point of using 3dFWHMx).
2314+
2315+
2316+
"""
2317+
_cmd = '3dFWHMx'
2318+
input_spec = FWHMxInputSpec
2319+
output_spec = FWHMxOutputSpec
2320+
_acf = True
2321+
2322+
def _parse_inputs(self, skip=None):
2323+
if not self.inputs.detrend:
2324+
if skip is None:
2325+
skip = []
2326+
skip += ['out_detrend']
2327+
return super(FWHMx, self)._parse_inputs(skip=skip)
2328+
2329+
def _format_arg(self, name, trait_spec, value):
2330+
if name == 'detrend':
2331+
if isinstance(value, bool):
2332+
if value:
2333+
return trait_spec.argstr
2334+
else:
2335+
return None
2336+
elif isinstance(value, int):
2337+
return trait_spec.argstr + ' %d' % value
2338+
2339+
if name == 'acf':
2340+
if isinstance(value, bool):
2341+
if value:
2342+
return trait_spec.argstr
2343+
else:
2344+
self._acf = False
2345+
return None
2346+
elif isinstance(value, tuple):
2347+
return trait_spec.argstr + ' %s %f' % value
2348+
elif isinstance(value, string_types):
2349+
return trait_spec.argstr + ' ' + value
2350+
return super(FWHMx, self)._format_arg(name, trait_spec, value)
2351+
2352+
def _list_outputs(self):
2353+
outputs = super(FWHMx, self)._list_outputs()
2354+
2355+
if self.inputs.detrend:
2356+
fname, ext = op.splitext(self.inputs.in_file)
2357+
if '.gz' in ext:
2358+
_, ext2 = op.splitext(fname)
2359+
ext = ext2 + ext
2360+
outputs['out_detrend'] += ext
2361+
else:
2362+
outputs['out_detrend'] = Undefined
2363+
2364+
sout = np.loadtxt(outputs['out_file']) #pylint: disable=E1101
2365+
if self._acf:
2366+
outputs['acf_param'] = tuple(sout[1])
2367+
sout = tuple(sout[0])
2368+
2369+
outputs['out_acf'] = op.abspath('3dFWHMx.1D')
2370+
if isinstance(self.inputs.acf, string_types):
2371+
outputs['out_acf'] = op.abspath(self.inputs.acf)
2372+
2373+
outputs['fwhm'] = tuple(sout)
2374+
return outputs
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from ....testing import assert_equal
3+
from ..base import AFNICommandBase
4+
5+
6+
def test_AFNICommandBase_inputs():
7+
input_map = dict(args=dict(argstr='%s',
8+
),
9+
environ=dict(nohash=True,
10+
usedefault=True,
11+
),
12+
ignore_exception=dict(nohash=True,
13+
usedefault=True,
14+
),
15+
terminal_output=dict(nohash=True,
16+
),
17+
)
18+
inputs = AFNICommandBase.input_spec()
19+
20+
for key, metadata in list(input_map.items()):
21+
for metakey, value in list(metadata.items()):
22+
yield assert_equal, getattr(inputs.traits()[key], metakey), value
23+

0 commit comments

Comments
 (0)