Skip to content

Commit 70a5128

Browse files
committed
Merge pull request #1090 from oesteban/enh/NewDipyInterfaces
[ENH] Add new dipy interfaces
2 parents 5406efc + 79ff663 commit 70a5128

23 files changed

+1113
-201
lines changed

CHANGES

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

4+
* ENH: New interfaces in dipy: RESTORE, EstimateResponseSH, CSD and StreamlineTractography
5+
(https://github.com/nipy/nipype/pull/1090)
46
* ENH: Added interfaces of AFNI (https://github.com/nipy/nipype/pull/1360,
57
https://github.com/nipy/nipype/pull/1361)
68
* ENH: Provides a Nipype wrapper for antsJointFusion (https://github.com/nipy/nipype/pull/1351)

circle.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies:
1919
# Set up python environment
2020
- pip install --upgrade pip
2121
- pip install -e .
22-
- pip install matplotlib sphinx ipython boto coverage
22+
- pip install matplotlib sphinx ipython boto coverage dipy
2323
- gem install fakes3
2424
- if [[ ! -d ~/examples/data ]]; then wget "http://tcpdiag.dl.sourceforge.net/project/nipy/nipype/nipype-0.2/nipype-tutorial.tar.bz2" && tar jxvf nipype-tutorial.tar.bz2 && mv nipype-tutorial/* ~/examples/; fi
2525
- if [[ ! -d ~/examples/fsl_course_data ]]; then wget -c "http://fsl.fmrib.ox.ac.uk/fslcourse/fdt1.tar.gz" && wget -c "http://fsl.fmrib.ox.ac.uk/fslcourse/fdt2.tar.gz" && wget -c "http://fsl.fmrib.ox.ac.uk/fslcourse/tbss.tar.gz" && mkdir ~/examples/fsl_course_data && tar zxvf fdt1.tar.gz -C ~/examples/fsl_course_data && tar zxvf fdt2.tar.gz -C ~/examples/fsl_course_data && tar zxvf tbss.tar.gz -C ~/examples/fsl_course_data; fi

nipype/interfaces/dipy/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from .tracks import TrackDensityMap
1+
from .tracks import StreamlineTractography, TrackDensityMap
22
from .tensors import TensorMode, DTI
33
from .preprocess import Resample, Denoise
4+
from .reconstruction import RESTORE, EstimateResponseSH, CSD
45
from .simulate import SimulateMultiTensor

nipype/interfaces/dipy/base.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# -*- coding: utf-8 -*-
2+
""" Base interfaces for dipy """
3+
import os.path as op
4+
import numpy as np
5+
from nipype.interfaces.base import (traits, File, isdefined,
6+
BaseInterface, BaseInterfaceInputSpec)
7+
from ... import logging
8+
9+
IFLOGGER = logging.getLogger('interface')
10+
11+
HAVE_DIPY = True
12+
try:
13+
import dipy
14+
except ImportError:
15+
HAVE_DIPY = False
16+
17+
18+
def no_dipy():
19+
""" Check if dipy is available """
20+
global HAVE_DIPY
21+
return not HAVE_DIPY
22+
23+
24+
def dipy_version():
25+
""" Check dipy version """
26+
if no_dipy():
27+
return None
28+
29+
return dipy.__version__
30+
31+
32+
class DipyBaseInterface(BaseInterface):
33+
34+
"""
35+
A base interface for py:mod:`dipy` computations
36+
"""
37+
def __init__(self, **inputs):
38+
if no_dipy():
39+
IFLOGGER.error('dipy was not found')
40+
# raise ImportError('dipy was not found')
41+
super(DipyBaseInterface, self).__init__(**inputs)
42+
43+
44+
class DipyBaseInterfaceInputSpec(BaseInterfaceInputSpec):
45+
in_file = File(exists=True, mandatory=True, desc=('input diffusion data'))
46+
in_bval = File(exists=True, mandatory=True, desc=('input b-values table'))
47+
in_bvec = File(exists=True, mandatory=True, desc=('input b-vectors table'))
48+
b0_thres = traits.Int(700, usedefault=True, desc=('b0 threshold'))
49+
out_prefix = traits.Str(desc=('output prefix for file names'))
50+
51+
52+
class DipyDiffusionInterface(DipyBaseInterface):
53+
54+
"""
55+
A base interface for py:mod:`dipy` computations
56+
"""
57+
input_spec = DipyBaseInterfaceInputSpec
58+
59+
def _get_gradient_table(self):
60+
bval = np.loadtxt(self.inputs.in_bval)
61+
bvec = np.loadtxt(self.inputs.in_bvec).T
62+
try:
63+
from dipy.data import GradientTable
64+
gtab = GradientTable(bvec)
65+
gtab.bvals = bval
66+
except NameError:
67+
from dipy.core.gradients import gradient_table
68+
gtab = gradient_table(bval, bvec)
69+
70+
gtab.b0_threshold = self.inputs.b0_thres
71+
return gtab
72+
73+
def _gen_filename(self, name, ext=None):
74+
fname, fext = op.splitext(op.basename(self.inputs.in_file))
75+
if fext == '.gz':
76+
fname, fext2 = op.splitext(fname)
77+
fext = fext2 + fext
78+
79+
if not isdefined(self.inputs.out_prefix):
80+
out_prefix = op.abspath(fname)
81+
else:
82+
out_prefix = self.inputs.out_prefix
83+
84+
if ext is None:
85+
ext = fext
86+
87+
return out_prefix + '_' + name + ext

nipype/interfaces/dipy/preprocess.py

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3-
"""Change directory to provide relative paths for doctests
3+
"""
4+
Change directory to provide relative paths for doctests
45
>>> import os
56
>>> filepath = os.path.dirname( os.path.realpath( __file__ ) )
67
>>> datadir = os.path.realpath(os.path.join(filepath, '../../testing/data'))
78
>>> os.chdir(datadir)
89
"""
910
import os.path as op
10-
import warnings
11-
1211
import nibabel as nb
1312
import numpy as np
1413

15-
from ..base import (traits, TraitedSpec, BaseInterface, File, isdefined)
16-
from ...utils.filemanip import split_filename
17-
from ...utils.misc import package_check
14+
from ..base import (traits, TraitedSpec, File, isdefined)
15+
from .base import DipyBaseInterface
1816
from ... import logging
19-
iflogger = logging.getLogger('interface')
20-
21-
have_dipy = True
22-
try:
23-
package_check('dipy', version='0.6.0')
24-
except Exception as e:
25-
have_dipy = False
17+
IFLOGGER = logging.getLogger('interface')
2618

2719

2820
class ResampleInputSpec(TraitedSpec):
@@ -33,15 +25,17 @@ class ResampleInputSpec(TraitedSpec):
3325
' is set, then isotropic regridding will '
3426
'be performed, with spacing equal to the '
3527
'smallest current zoom.'))
36-
interp = traits.Int(1, mandatory=True, usedefault=True, desc=('order of '
37-
'the interpolator (0 = nearest, 1 = linear, etc.'))
28+
interp = traits.Int(
29+
1, mandatory=True, usedefault=True,
30+
desc=('order of the interpolator (0 = nearest, 1 = linear, etc.'))
3831

3932

4033
class ResampleOutputSpec(TraitedSpec):
4134
out_file = File(exists=True)
4235

4336

44-
class Resample(BaseInterface):
37+
class Resample(DipyBaseInterface):
38+
4539
"""
4640
An interface to reslicing diffusion datasets.
4741
See
@@ -69,7 +63,7 @@ def _run_interface(self, runtime):
6963
resample_proxy(self.inputs.in_file, order=order,
7064
new_zooms=vox_size, out_file=out_file)
7165

72-
iflogger.info('Resliced image saved as {i}'.format(i=out_file))
66+
IFLOGGER.info('Resliced image saved as {i}'.format(i=out_file))
7367
return runtime
7468

7569
def _list_outputs(self):
@@ -92,17 +86,21 @@ class DenoiseInputSpec(TraitedSpec):
9286
noise_model = traits.Enum('rician', 'gaussian', mandatory=True,
9387
usedefault=True,
9488
desc=('noise distribution model'))
89+
signal_mask = File(desc=('mask in which the mean signal '
90+
'will be computed'), exists=True)
9591
noise_mask = File(desc=('mask in which the standard deviation of noise '
9692
'will be computed'), exists=True)
9793
patch_radius = traits.Int(1, desc='patch radius')
9894
block_radius = traits.Int(5, desc='block_radius')
95+
snr = traits.Float(desc='manually set an SNR')
9996

10097

10198
class DenoiseOutputSpec(TraitedSpec):
10299
out_file = File(exists=True)
103100

104101

105-
class Denoise(BaseInterface):
102+
class Denoise(DipyBaseInterface):
103+
106104
"""
107105
An interface to denoising diffusion datasets [Coupe2008]_.
108106
See
@@ -140,16 +138,24 @@ def _run_interface(self, runtime):
140138
if isdefined(self.inputs.block_radius):
141139
settings['block_radius'] = self.inputs.block_radius
142140

141+
snr = None
142+
if isdefined(self.inputs.snr):
143+
snr = self.inputs.snr
144+
145+
signal_mask = None
146+
if isdefined(self.inputs.signal_mask):
147+
signal_mask = nb.load(self.inputs.signal_mask).get_data()
143148
noise_mask = None
144-
if isdefined(self.inputs.in_mask):
149+
if isdefined(self.inputs.noise_mask):
145150
noise_mask = nb.load(self.inputs.noise_mask).get_data()
146151

147-
_, s = nlmeans_proxy(self.inputs.in_file,
148-
settings,
149-
noise_mask=noise_mask,
152+
_, s = nlmeans_proxy(self.inputs.in_file, settings,
153+
snr=snr,
154+
smask=signal_mask,
155+
nmask=noise_mask,
150156
out_file=out_file)
151-
iflogger.info(('Denoised image saved as {i}, estimated '
152-
'sigma={s}').format(i=out_file, s=s))
157+
IFLOGGER.info(('Denoised image saved as {i}, estimated '
158+
'SNR={s}').format(i=out_file, s=str(s)))
153159
return runtime
154160

155161
def _list_outputs(self):
@@ -169,7 +175,7 @@ def resample_proxy(in_file, order=3, new_zooms=None, out_file=None):
169175
"""
170176
Performs regridding of an image to set isotropic voxel sizes using dipy.
171177
"""
172-
from dipy.align.aniso2iso import resample
178+
from dipy.align.reslice import reslice
173179

174180
if out_file is None:
175181
fname, fext = op.splitext(op.basename(in_file))
@@ -191,7 +197,7 @@ def resample_proxy(in_file, order=3, new_zooms=None, out_file=None):
191197
if np.all(im_zooms == new_zooms):
192198
return in_file
193199

194-
data2, affine2 = resample(data, affine, im_zooms, new_zooms, order=order)
200+
data2, affine2 = reslice(data, affine, im_zooms, new_zooms, order=order)
195201
tmp_zooms = np.array(hdr.get_zooms())
196202
tmp_zooms[:3] = new_zooms[0]
197203
hdr.set_zooms(tuple(tmp_zooms))
@@ -203,12 +209,16 @@ def resample_proxy(in_file, order=3, new_zooms=None, out_file=None):
203209

204210

205211
def nlmeans_proxy(in_file, settings,
206-
noise_mask=None, out_file=None):
212+
snr=None,
213+
smask=None,
214+
nmask=None,
215+
out_file=None):
207216
"""
208217
Uses non-local means to denoise 4D datasets
209218
"""
210-
package_check('dipy', version='0.8.0.dev')
211219
from dipy.denoise.nlmeans import nlmeans
220+
from scipy.ndimage.morphology import binary_erosion
221+
from scipy import ndimage
212222

213223
if out_file is None:
214224
fname, fext = op.splitext(op.basename(in_file))
@@ -222,13 +232,69 @@ def nlmeans_proxy(in_file, settings,
222232
data = img.get_data()
223233
aff = img.affine
224234

225-
nmask = data[..., 0] > 80
226-
if noise_mask is not None:
227-
nmask = noise_mask > 0
228-
229-
sigma = np.std(data[nmask == 1])
230-
den = nlmeans(data, sigma, **settings)
235+
if data.ndim < 4:
236+
data = data[..., np.newaxis]
237+
238+
data = np.nan_to_num(data)
239+
240+
if data.max() < 1.0e-4:
241+
raise RuntimeError('There is no signal in the image')
242+
243+
df = 1.0
244+
if data.max() < 1000.0:
245+
df = 1000. / data.max()
246+
data *= df
247+
248+
b0 = data[..., 0]
249+
250+
if smask is None:
251+
smask = np.zeros_like(b0)
252+
smask[b0 > np.percentile(b0, 85.)] = 1
253+
254+
smask = binary_erosion(
255+
smask.astype(np.uint8), iterations=2).astype(np.uint8)
256+
257+
if nmask is None:
258+
nmask = np.ones_like(b0, dtype=np.uint8)
259+
bmask = settings['mask']
260+
if bmask is None:
261+
bmask = np.zeros_like(b0)
262+
bmask[b0 > np.percentile(b0[b0 > 0], 10)] = 1
263+
label_im, nb_labels = ndimage.label(bmask)
264+
sizes = ndimage.sum(bmask, label_im, range(nb_labels + 1))
265+
maxidx = np.argmax(sizes)
266+
bmask = np.zeros_like(b0, dtype=np.uint8)
267+
bmask[label_im == maxidx] = 1
268+
nmask[bmask > 0] = 0
269+
else:
270+
nmask = np.squeeze(nmask)
271+
nmask[nmask > 0.0] = 1
272+
nmask[nmask < 1] = 0
273+
nmask = nmask.astype(bool)
274+
275+
nmask = binary_erosion(nmask, iterations=1).astype(np.uint8)
276+
277+
den = np.zeros_like(data)
278+
279+
est_snr = True
280+
if snr is not None:
281+
snr = [snr] * data.shape[-1]
282+
est_snr = False
283+
else:
284+
snr = []
285+
286+
for i in range(data.shape[-1]):
287+
d = data[..., i]
288+
if est_snr:
289+
s = np.mean(d[smask > 0])
290+
n = np.std(d[nmask > 0])
291+
snr.append(s / n)
292+
293+
den[..., i] = nlmeans(d, snr[i], **settings)
294+
295+
den = np.squeeze(den)
296+
den /= df
231297

232298
nb.Nifti1Image(den.astype(hdr.get_data_dtype()), aff,
233299
hdr).to_filename(out_file)
234-
return out_file, sigma
300+
return out_file, snr

0 commit comments

Comments
 (0)