Skip to content

Commit 8a0dcd0

Browse files
committed
Merge pull request #3 from nipy/master
Update to current master
2 parents 566eaa3 + dbb6029 commit 8a0dcd0

13 files changed

+418
-24
lines changed

nipype/interfaces/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,9 +1349,9 @@ def _terminal_output_update(self):
13491349
def set_default_terminal_output(cls, output_type):
13501350
"""Set the default terminal output for CommandLine Interfaces.
13511351
1352-
This method is used to set default terminal output for
1353-
CommandLine Interfaces. However, setting this will not
1354-
update the output type for any existing instances. For these,
1352+
This method is used to set default terminal output for
1353+
CommandLine Interfaces. However, setting this will not
1354+
update the output type for any existing instances. For these,
13551355
assign the <instance>.inputs.terminal_output.
13561356
"""
13571357

nipype/interfaces/dipy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .tracks import TrackDensityMap
22
from .tensors import TensorMode, DTI
33
from .preprocess import Resample, Denoise
4+
from .simulate import SimulateMultiTensor

nipype/interfaces/dipy/simulate.py

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
# -*- coding: utf-8 -*-
2+
"""Change directory to provide relative paths for doctests
3+
>>> import os
4+
>>> filepath = os.path.dirname( os.path.realpath( __file__ ) )
5+
>>> datadir = os.path.realpath(os.path.join(filepath, '../../testing/data'))
6+
>>> os.chdir(datadir)
7+
"""
8+
9+
from nipype.interfaces.base import (
10+
traits, TraitedSpec, BaseInterface, BaseInterfaceInputSpec, File,
11+
InputMultiPath, isdefined)
12+
from nipype.utils.filemanip import split_filename
13+
import os.path as op
14+
import nibabel as nb
15+
import numpy as np
16+
from nipype.utils.misc import package_check
17+
import warnings
18+
19+
from multiprocessing import (Process, Pool, cpu_count, pool,
20+
Manager, TimeoutError)
21+
22+
from ... import logging
23+
iflogger = logging.getLogger('interface')
24+
25+
have_dipy = True
26+
try:
27+
package_check('dipy', version='0.8.0')
28+
except Exception, e:
29+
have_dipy = False
30+
else:
31+
import numpy as np
32+
from dipy.sims.voxel import (multi_tensor, add_noise,
33+
all_tensor_evecs)
34+
from dipy.core.gradients import gradient_table
35+
36+
37+
class SimulateMultiTensorInputSpec(BaseInterfaceInputSpec):
38+
in_dirs = InputMultiPath(File(exists=True), mandatory=True,
39+
desc='list of fibers (principal directions)')
40+
in_frac = InputMultiPath(File(exists=True), mandatory=True,
41+
desc=('volume fraction of each fiber'))
42+
in_vfms = InputMultiPath(File(exists=True), mandatory=True,
43+
desc=('volume fractions of isotropic '
44+
'compartiments'))
45+
in_mask = File(exists=True, desc='mask to simulate data')
46+
47+
diff_iso = traits.List(
48+
[3000e-6, 960e-6, 680e-6], traits.Float, usedefault=True,
49+
desc='Diffusivity of isotropic compartments')
50+
diff_sf = traits.Tuple(
51+
(1700e-6, 200e-6, 200e-6),
52+
traits.Float, traits.Float, traits.Float, usedefault=True,
53+
desc='Single fiber tensor')
54+
55+
n_proc = traits.Int(0, usedefault=True, desc='number of processes')
56+
baseline = File(exists=True, mandatory=True, desc='baseline T2 signal')
57+
gradients = File(exists=True, desc='gradients file')
58+
in_bvec = File(exists=True, desc='input bvecs file')
59+
in_bval = File(exists=True, desc='input bvals file')
60+
num_dirs = traits.Int(32, usedefault=True,
61+
desc=('number of gradient directions (when table '
62+
'is automatically generated)'))
63+
bvalues = traits.List(traits.Int, value=[1000, 3000], usedefault=True,
64+
desc=('list of b-values (when table '
65+
'is automatically generated)'))
66+
out_file = File('sim_dwi.nii.gz', usedefault=True,
67+
desc='output file with fractions to be simluated')
68+
out_mask = File('sim_msk.nii.gz', usedefault=True,
69+
desc='file with the mask simulated')
70+
out_bvec = File('bvec.sim', usedefault=True, desc='simulated b vectors')
71+
out_bval = File('bval.sim', usedefault=True, desc='simulated b values')
72+
snr = traits.Int(0, usedefault=True, desc='signal-to-noise ratio (dB)')
73+
74+
75+
class SimulateMultiTensorOutputSpec(TraitedSpec):
76+
out_file = File(exists=True, desc='simulated DWIs')
77+
out_mask = File(exists=True, desc='mask file')
78+
out_bvec = File(exists=True, desc='simulated b vectors')
79+
out_bval = File(exists=True, desc='simulated b values')
80+
81+
82+
class SimulateMultiTensor(BaseInterface):
83+
84+
"""
85+
Interface to MultiTensor model simulator in dipy
86+
http://nipy.org/dipy/examples_built/simulate_multi_tensor.html
87+
88+
Example
89+
-------
90+
91+
>>> import nipype.interfaces.dipy as dipy
92+
>>> sim = dipy.SimulateMultiTensor()
93+
>>> sim.inputs.in_dirs = ['fdir00.nii', 'fdir01.nii']
94+
>>> sim.inputs.in_frac = ['ffra00.nii', 'ffra01.nii']
95+
>>> sim.inputs.in_vfms = ['tpm_00.nii.gz', 'tpm_01.nii.gz',
96+
... 'tpm_02.nii.gz']
97+
>>> sim.inputs.baseline = 'b0.nii'
98+
>>> sim.inputs.in_bvec = 'bvecs'
99+
>>> sim.inputs.in_bval = 'bvals'
100+
>>> sim.run() # doctest: +SKIP
101+
"""
102+
input_spec = SimulateMultiTensorInputSpec
103+
output_spec = SimulateMultiTensorOutputSpec
104+
105+
def _run_interface(self, runtime):
106+
# Gradient table
107+
if isdefined(self.inputs.in_bval) and isdefined(self.inputs.in_bvec):
108+
# Load the gradient strengths and directions
109+
bvals = np.loadtxt(self.inputs.in_bval)
110+
bvecs = np.loadtxt(self.inputs.in_bvec).T
111+
gtab = gradient_table(bvals, bvecs)
112+
else:
113+
gtab = _generate_gradients(self.inputs.num_dirs,
114+
self.inputs.bvalues)
115+
ndirs = len(gtab.bvals)
116+
np.savetxt(op.abspath(self.inputs.out_bvec), gtab.bvecs.T)
117+
np.savetxt(op.abspath(self.inputs.out_bval), gtab.bvals)
118+
119+
# Load the baseline b0 signal
120+
b0_im = nb.load(self.inputs.baseline)
121+
hdr = b0_im.get_header()
122+
shape = b0_im.get_shape()
123+
aff = b0_im.get_affine()
124+
125+
# Check and load sticks and their volume fractions
126+
nsticks = len(self.inputs.in_dirs)
127+
if len(self.inputs.in_frac) != nsticks:
128+
raise RuntimeError(('Number of sticks and their volume fractions'
129+
' must match.'))
130+
131+
# Volume fractions of isotropic compartments
132+
nballs = len(self.inputs.in_vfms)
133+
vfs = np.squeeze(nb.concat_images(
134+
[nb.load(f) for f in self.inputs.in_vfms]).get_data())
135+
if nballs == 1:
136+
vfs = vfs[..., np.newaxis]
137+
total_vf = np.sum(vfs, axis=3)
138+
139+
# Generate a mask
140+
if isdefined(self.inputs.in_mask):
141+
msk = nb.load(self.inputs.in_mask).get_data()
142+
msk[msk > 0.0] = 1.0
143+
msk[msk < 1.0] = 0.0
144+
else:
145+
msk = np.zeros(shape)
146+
msk[total_vf > 0.0] = 1.0
147+
148+
msk = np.clip(msk, 0.0, 1.0)
149+
nvox = len(msk[msk > 0])
150+
151+
# Fiber fractions
152+
ffsim = nb.concat_images([nb.load(f) for f in self.inputs.in_frac])
153+
ffs = np.nan_to_num(np.squeeze(ffsim.get_data())) # fiber fractions
154+
ffs = np.clip(ffs, 0., 1.)
155+
if nsticks == 1:
156+
ffs = ffs[..., np.newaxis]
157+
158+
for i in range(nsticks):
159+
ffs[..., i] *= msk
160+
161+
total_ff = np.sum(ffs, axis=3)
162+
163+
# Fix incongruencies in fiber fractions
164+
for i in range(1, nsticks):
165+
if np.any(total_ff > 1.0):
166+
errors = np.zeros_like(total_ff)
167+
errors[total_ff > 1.0] = total_ff[total_ff > 1.0] - 1.0
168+
ffs[..., i] -= errors
169+
ffs[ffs < 0.0] = 0.0
170+
total_ff = np.sum(ffs, axis=3)
171+
172+
for i in range(vfs.shape[-1]):
173+
vfs[..., i] -= total_ff
174+
vfs = np.clip(vfs, 0., 1.)
175+
176+
fractions = np.concatenate((ffs, vfs), axis=3)
177+
178+
nb.Nifti1Image(fractions, aff, None).to_filename('fractions.nii.gz')
179+
nb.Nifti1Image(np.sum(fractions, axis=3), aff, None).to_filename(
180+
'total_vf.nii.gz')
181+
182+
mhdr = hdr.copy()
183+
mhdr.set_data_dtype(np.uint8)
184+
mhdr.set_xyzt_units('mm', 'sec')
185+
nb.Nifti1Image(msk, aff, mhdr).to_filename(
186+
op.abspath(self.inputs.out_mask))
187+
188+
# Initialize stack of args
189+
fracs = fractions[msk > 0]
190+
191+
# Stack directions
192+
dirs = None
193+
for i in range(nsticks):
194+
f = self.inputs.in_dirs[i]
195+
fd = np.nan_to_num(nb.load(f).get_data())
196+
w = np.linalg.norm(fd, axis=3)[..., np.newaxis]
197+
w[w < np.finfo(float).eps] = 1.0
198+
fd /= w
199+
if dirs is None:
200+
dirs = fd[msk > 0].copy()
201+
else:
202+
dirs = np.hstack((dirs, fd[msk > 0]))
203+
204+
# Add random directions for isotropic components
205+
for d in range(nballs):
206+
fd = np.random.randn(nvox, 3)
207+
w = np.linalg.norm(fd, axis=1)
208+
fd[w < np.finfo(float).eps, ...] = np.array([1., 0., 0.])
209+
w[w < np.finfo(float).eps] = 1.0
210+
fd /= w[..., np.newaxis]
211+
dirs = np.hstack((dirs, fd))
212+
213+
sf_evals = list(self.inputs.diff_sf)
214+
ba_evals = list(self.inputs.diff_iso)
215+
216+
mevals = [sf_evals] * nsticks + \
217+
[[ba_evals[d]] * 3 for d in range(nballs)]
218+
219+
b0 = b0_im.get_data()[msk > 0]
220+
args = []
221+
for i in range(nvox):
222+
args.append(
223+
{'fractions': fracs[i, ...].tolist(),
224+
'sticks': [tuple(dirs[i, j:j + 3])
225+
for j in range(nsticks + nballs)],
226+
'gradients': gtab,
227+
'mevals': mevals,
228+
'S0': b0[i],
229+
'snr': self.inputs.snr
230+
})
231+
232+
n_proc = self.inputs.n_proc
233+
if n_proc == 0:
234+
n_proc = cpu_count()
235+
236+
try:
237+
pool = Pool(processes=n_proc, maxtasksperchild=50)
238+
except TypeError:
239+
pool = Pool(processes=n_proc)
240+
241+
# Simulate sticks using dipy
242+
iflogger.info(('Starting simulation of %d voxels, %d diffusion'
243+
' directions.') % (len(args), ndirs))
244+
result = np.array(pool.map(_compute_voxel, args))
245+
if np.shape(result)[1] != ndirs:
246+
raise RuntimeError(('Computed directions do not match number'
247+
'of b-values.'))
248+
249+
signal = np.zeros((shape[0], shape[1], shape[2], ndirs))
250+
signal[msk > 0] = result
251+
252+
simhdr = hdr.copy()
253+
simhdr.set_data_dtype(np.float32)
254+
simhdr.set_xyzt_units('mm', 'sec')
255+
nb.Nifti1Image(signal.astype(np.float32), aff,
256+
simhdr).to_filename(op.abspath(self.inputs.out_file))
257+
258+
return runtime
259+
260+
def _list_outputs(self):
261+
outputs = self._outputs().get()
262+
outputs['out_file'] = op.abspath(self.inputs.out_file)
263+
outputs['out_mask'] = op.abspath(self.inputs.out_mask)
264+
outputs['out_bvec'] = op.abspath(self.inputs.out_bvec)
265+
outputs['out_bval'] = op.abspath(self.inputs.out_bval)
266+
267+
return outputs
268+
269+
270+
def _compute_voxel(args):
271+
"""
272+
Simulate DW signal for one voxel. Uses the multi-tensor model and
273+
three isotropic compartments.
274+
275+
Apparent diffusivity tensors are taken from [Alexander2002]_
276+
and [Pierpaoli1996]_.
277+
278+
.. [Alexander2002] Alexander et al., Detection and modeling of non-Gaussian
279+
apparent diffusion coefficient profiles in human brain data, MRM
280+
48(2):331-340, 2002, doi: `10.1002/mrm.10209
281+
<http://dx.doi.org/10.1002/mrm.10209>`_.
282+
.. [Pierpaoli1996] Pierpaoli et al., Diffusion tensor MR imaging
283+
of the human brain, Radiology 201:637-648. 1996.
284+
"""
285+
286+
ffs = args['fractions']
287+
gtab = args['gradients']
288+
signal = np.zeros_like(gtab.bvals, dtype=np.float32)
289+
290+
# Simulate dwi signal
291+
sf_vf = np.sum(ffs)
292+
if sf_vf > 0.0:
293+
ffs = ((np.array(ffs) / sf_vf) * 100)
294+
snr = args['snr'] if args['snr'] > 0 else None
295+
296+
try:
297+
signal, _ = multi_tensor(
298+
gtab, args['mevals'], S0=args['S0'],
299+
angles=args['sticks'], fractions=ffs, snr=snr)
300+
except Exception as e:
301+
pass
302+
# iflogger.warn('Exception simulating dwi signal: %s' % e)
303+
304+
return signal.tolist()
305+
306+
307+
def _generate_gradients(ndirs=64, values=[1000, 3000], nb0s=1):
308+
"""
309+
Automatically generate a `gradient table
310+
<http://nipy.org/dipy/examples_built/gradients_spheres.html#example-gradients-spheres>`_
311+
312+
"""
313+
import numpy as np
314+
from dipy.core.sphere import (disperse_charges, Sphere, HemiSphere)
315+
from dipy.core.gradients import gradient_table
316+
317+
theta = np.pi * np.random.rand(ndirs)
318+
phi = 2 * np.pi * np.random.rand(ndirs)
319+
hsph_initial = HemiSphere(theta=theta, phi=phi)
320+
hsph_updated, potential = disperse_charges(hsph_initial, 5000)
321+
322+
values = np.atleast_1d(values).tolist()
323+
vertices = hsph_updated.vertices
324+
bvecs = vertices.copy()
325+
bvals = np.ones(vertices.shape[0]) * values[0]
326+
327+
for v in values[1:]:
328+
bvecs = np.vstack((bvecs, vertices))
329+
bvals = np.hstack((bvals, v * np.ones(vertices.shape[0])))
330+
331+
for i in xrange(0, nb0s):
332+
bvals = bvals.tolist()
333+
bvals.insert(0, 0)
334+
335+
bvecs = bvecs.tolist()
336+
bvecs.insert(0, np.zeros(3))
337+
338+
return gradient_table(bvals, bvecs)

0 commit comments

Comments
 (0)