Skip to content

Commit c8792dc

Browse files
committed
Merge remote-tracking branch 'upstream/master' into enh/restingconn
* upstream/master: revert MCR related change change location of templates and gaussian parameters for SPM12 segment SPM12 outputs .nii instead of .img/.hdr BF: include nipype.workflows.data also as a data_dir into setup.py BF: include nipype.workflows.data into setup.py fix: clean up auto tests fix: remove code added by oscar fix: many miscellaneous fixes - mostly styles - but also for doc building the ipython extension has moved to ipython fix: resolved conflict typo in nipype/workflows/dmri/fsl/epi.py fix: change where nlmeans check is made Add tests moved the import inside the constructor Fixes #933 Update test functions BEFORE tests fix: topup when out_base is a path [algorithms] Overlap interface revision
2 parents 4ec7abd + d9a52d0 commit c8792dc

24 files changed

+351
-129
lines changed

CHANGES

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Next Release
1818
* ENH: New Diffusion Toolkit interface: TrackMerge
1919
* ENH: New MRtrix interface: FilterTracks
2020
* ENH: New metrics group in algorithms. Now Distance, Overlap, and FuzzyOverlap
21-
are found in nipype.algorithms.metrics instead of misc
21+
are found in nipype.algorithms.metrics instead of misc. Overlap interface
22+
extended to allow files containing multiple ROIs and volume physical units.
2223
* ENH: New interface in algorithms.metrics: ErrorMap (a voxel-wise diff map).
2324
* ENH: New FreeSurfer workflow: create_skullstripped_recon_flow()
2425
* ENH: Deep revision of workflows for correction of dMRI artifacts. New dmri_preprocessing

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ specs:
7070
@echo "Checking specs and autogenerating spec tests"
7171
python tools/checkspecs.py
7272

73-
check-before-commit: trailing-spaces html test specs
73+
check-before-commit: specs trailing-spaces html test
7474
@echo "removed spaces"
7575
@echo "built docs"
7676
@echo "ran test"

doc/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
'numpy_ext.numpydoc',
4040
'matplotlib.sphinxext.plot_directive',
4141
'matplotlib.sphinxext.only_directives',
42-
'matplotlib.sphinxext.ipython_directive',
42+
'IPython.sphinxext.ipython_directive',
4343
'ipython_console_highlighting'
4444
]
4545

examples/fmri_spm.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import os # system functions
1919

2020
from nipype import config
21-
config.enable_provenance()
21+
#config.enable_provenance()
2222

2323
from nipype.interfaces import spm, fsl
2424

@@ -349,7 +349,8 @@ def getstripdir(subject_id):
349349
# collect all the con images for each contrast.
350350
contrast_ids = range(1,len(contrasts)+1)
351351
l2source = pe.Node(nio.DataGrabber(infields=['fwhm', 'con']), name="l2source")
352-
l2source.inputs.template=os.path.abspath('spm_tutorial/l1output/*/con*/*/_fwhm_%d/con_%04d.img')
352+
# we use .*i* to capture both .img (SPM8) and .nii (SPM12)
353+
l2source.inputs.template=os.path.abspath('spm_tutorial/l1output/*/con*/*/_fwhm_%d/con_%04d.*i*')
353354
# iterate over all contrast images
354355
l2source.iterables = [('fwhm',fwhmlist),
355356
('con',contrast_ids)]

examples/fmri_spm_dartel.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,8 @@ def getstripdir(subject_id):
481481
# collect all the con images for each contrast.
482482
contrast_ids = range(1,len(contrasts)+1)
483483
l2source = pe.Node(nio.DataGrabber(infields=['fwhm', 'con']), name="l2source")
484-
l2source.inputs.template=os.path.abspath('spm_dartel_tutorial/l1output/*/con*/*/_fwhm_%d/con_%04d.img')
484+
# we use .*i* to capture both .img (SPM8) and .nii (SPM12)
485+
l2source.inputs.template=os.path.abspath('spm_dartel_tutorial/l1output/*/con*/*/_fwhm_%d/con_%04d.*i*')
485486
# iterate over all contrast images
486487
l2source.iterables = [('fwhm',fwhmlist),
487488
('con',contrast_ids)]

examples/fmri_spm_nested.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ def getstripdir(subject_id):
419419
"""
420420

421421
if __name__ == '__main__':
422-
level1.run()
422+
level1.run('MultiProc')
423423
level1.write_graph()
424424

425425
"""
@@ -435,7 +435,8 @@ def getstripdir(subject_id):
435435
# collect all the con images for each contrast.
436436
contrast_ids = range(1,len(contrasts)+1)
437437
l2source = pe.Node(nio.DataGrabber(infields=['fwhm', 'con']), name="l2source")
438-
l2source.inputs.template=os.path.abspath('spm_tutorial2/l1output/*/con*/*/_fwhm_%d/con_%04d.img')
438+
# we use .*i* to capture both .img (SPM8) and .nii (SPM12)
439+
l2source.inputs.template=os.path.abspath('spm_tutorial2/l1output/*/con*/*/_fwhm_%d/con_%04d.*i*')
439440
# iterate over all contrast images
440441
l2source.iterables = [('fwhm',fwhmlist),
441442
('con',contrast_ids)]
@@ -477,5 +478,5 @@ def getstripdir(subject_id):
477478
"""
478479

479480
if __name__ == '__main__':
480-
l2pipeline.run()
481+
l2pipeline.run('MultiProc')
481482

nipype/algorithms/metrics.py

Lines changed: 119 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
iflogger = logging.getLogger('interface')
3838

3939

40-
4140
class DistanceInputSpec(BaseInterfaceInputSpec):
4241
volume1 = File(exists=True, mandatory=True,
4342
desc="Has to have the same dimensions as volume2.")
@@ -104,7 +103,10 @@ def _eucl_min(self, nii1, nii2):
104103
dist_matrix = cdist(set1_coordinates.T, set2_coordinates.T)
105104
(point1, point2) = np.unravel_index(
106105
np.argmin(dist_matrix), dist_matrix.shape)
107-
return (euclidean(set1_coordinates.T[point1, :], set2_coordinates.T[point2, :]), set1_coordinates.T[point1, :], set2_coordinates.T[point2, :])
106+
return (euclidean(set1_coordinates.T[point1, :],
107+
set2_coordinates.T[point2, :]),
108+
set1_coordinates.T[point1, :],
109+
set2_coordinates.T[point2, :])
108110

109111
def _eucl_cog(self, nii1, nii2):
110112
origdata1 = nii1.get_data().astype(np.bool)
@@ -216,38 +218,64 @@ def _list_outputs(self):
216218

217219
class OverlapInputSpec(BaseInterfaceInputSpec):
218220
volume1 = File(exists=True, mandatory=True,
219-
desc="Has to have the same dimensions as volume2.")
221+
desc='Has to have the same dimensions as volume2.')
220222
volume2 = File(exists=True, mandatory=True,
221-
desc="Has to have the same dimensions as volume1.")
222-
mask_volume = File(
223-
exists=True, desc="calculate overlap only within this mask.")
224-
out_file = File("diff.nii", usedefault=True)
223+
desc='Has to have the same dimensions as volume1.')
224+
mask_volume = File(exists=True,
225+
desc='calculate overlap only within this mask.')
226+
bg_overlap = traits.Bool(False, usedefault=True, mandatory=True,
227+
desc='consider zeros as a label')
228+
out_file = File('diff.nii', usedefault=True)
229+
weighting = traits.Enum('none', 'volume', 'squared_vol', usedefault=True,
230+
desc=('\'none\': no class-overlap weighting is '
231+
'performed. \'volume\': computed class-'
232+
'overlaps are weighted by class volume '
233+
'\'squared_vol\': computed class-overlaps '
234+
'are weighted by the squared volume of '
235+
'the class'))
236+
vol_units = traits.Enum('voxel', 'mm', mandatory=True, usedefault=True,
237+
desc='units for volumes')
225238

226239

227240
class OverlapOutputSpec(TraitedSpec):
228-
jaccard = traits.Float()
229-
dice = traits.Float()
230-
volume_difference = traits.Int()
231-
diff_file = File(exists=True)
241+
jaccard = traits.Float(desc='averaged jaccard index')
242+
dice = traits.Float(desc='averaged dice index')
243+
roi_ji = traits.List(traits.Float(),
244+
desc=('the Jaccard index (JI) per ROI'))
245+
roi_di = traits.List(traits.Float(), desc=('the Dice index (DI) per ROI'))
246+
volume_difference = traits.Float(desc=('averaged volume difference'))
247+
roi_voldiff = traits.List(traits.Float(),
248+
desc=('volume differences of ROIs'))
249+
labels = traits.List(traits.Int(),
250+
desc=('detected labels'))
251+
diff_file = File(exists=True,
252+
desc='error map of differences')
232253

233254

234255
class Overlap(BaseInterface):
235-
"""Calculates various overlap measures between two maps.
256+
"""
257+
Calculates Dice and Jaccard's overlap measures between two ROI maps.
258+
The interface is backwards compatible with the former version in
259+
which only binary files were accepted.
260+
261+
The averaged values of overlap indices can be weighted. Volumes
262+
now can be reported in :math:`mm^3`, although they are given in voxels
263+
to keep backwards compatibility.
236264
237265
Example
238266
-------
239267
240268
>>> overlap = Overlap()
241269
>>> overlap.inputs.volume1 = 'cont1.nii'
242-
>>> overlap.inputs.volume1 = 'cont2.nii'
270+
>>> overlap.inputs.volume2 = 'cont2.nii'
243271
>>> res = overlap.run() # doctest: +SKIP
244-
"""
245272
273+
"""
246274
input_spec = OverlapInputSpec
247275
output_spec = OverlapOutputSpec
248276

249277
def _bool_vec_dissimilarity(self, booldata1, booldata2, method):
250-
methods = {"dice": dice, "jaccard": jaccard}
278+
methods = {'dice': dice, 'jaccard': jaccard}
251279
if not (np.any(booldata1) or np.any(booldata2)):
252280
return 0
253281
return 1 - methods[method](booldata1.flat, booldata2.flat)
@@ -256,59 +284,105 @@ def _run_interface(self, runtime):
256284
nii1 = nb.load(self.inputs.volume1)
257285
nii2 = nb.load(self.inputs.volume2)
258286

259-
origdata1 = np.logical_not(
260-
np.logical_or(nii1.get_data() == 0, np.isnan(nii1.get_data())))
261-
origdata2 = np.logical_not(
262-
np.logical_or(nii2.get_data() == 0, np.isnan(nii2.get_data())))
287+
scale = 1.0
263288

264-
if isdefined(self.inputs.mask_volume):
265-
maskdata = nb.load(self.inputs.mask_volume).get_data()
266-
maskdata = np.logical_not(
267-
np.logical_or(maskdata == 0, np.isnan(maskdata)))
268-
origdata1 = np.logical_and(maskdata, origdata1)
269-
origdata2 = np.logical_and(maskdata, origdata2)
289+
if self.inputs.vol_units == 'mm':
290+
voxvol = nii1.get_header().get_zooms()
291+
for i in xrange(nii1.get_data().ndim-1):
292+
scale = scale * voxvol[i]
270293

271-
for method in ("dice", "jaccard"):
272-
setattr(self, '_' + method, self._bool_vec_dissimilarity(
273-
origdata1, origdata2, method=method))
294+
data1 = nii1.get_data()
295+
data1[np.logical_or(data1 < 0, np.isnan(data1))] = 0
296+
max1 = int(data1.max())
297+
data1 = data1.astype(np.min_scalar_type(max1))
298+
data2 = nii2.get_data().astype(np.min_scalar_type(max1))
299+
data2[np.logical_or(data1 < 0, np.isnan(data1))] = 0
300+
max2 = data2.max()
301+
maxlabel = max(max1, max2)
274302

275-
self._volume = int(origdata1.sum() - origdata2.sum())
303+
if isdefined(self.inputs.mask_volume):
304+
maskdata = nb.load(self.inputs.mask_volume).get_data()
305+
maskdata = ~np.logical_or(maskdata == 0, np.isnan(maskdata))
306+
data1[~maskdata] = 0
307+
data2[~maskdata] = 0
308+
309+
res = []
310+
volumes1 = []
311+
volumes2 = []
312+
313+
labels = np.unique(data1[data1 > 0].reshape(-1)).tolist()
314+
if self.inputs.bg_overlap:
315+
labels.insert(0, 0)
316+
317+
for l in labels:
318+
res.append(self._bool_vec_dissimilarity(data1 == l,
319+
data2 == l, method='jaccard'))
320+
volumes1.append(scale * len(data1[data1 == l]))
321+
volumes2.append(scale * len(data2[data2 == l]))
322+
323+
results = dict(jaccard=[], dice=[])
324+
results['jaccard'] = np.array(res)
325+
results['dice'] = 2.0*results['jaccard'] / (results['jaccard'] + 1.0)
326+
327+
weights = np.ones((len(volumes1),), dtype=np.float32)
328+
if self.inputs.weighting != 'none':
329+
weights = weights / np.array(volumes1)
330+
if self.inputs.weighting == 'squared_vol':
331+
weights = weights**2
332+
weights = weights / np.sum(weights)
276333

277-
both_data = np.zeros(origdata1.shape)
278-
both_data[origdata1] = 1
279-
both_data[origdata2] += 2
334+
both_data = np.zeros(data1.shape)
335+
both_data[(data1 - data2) != 0] = 1
280336

281337
nb.save(nb.Nifti1Image(both_data, nii1.get_affine(),
282338
nii1.get_header()), self.inputs.out_file)
283339

340+
self._labels = labels
341+
self._ove_rois = results
342+
self._vol_rois = ((np.array(volumes1) - np.array(volumes2)) /
343+
np.array(volumes1))
344+
345+
self._dice = round(np.sum(weights*results['dice']), 5)
346+
self._jaccard = round(np.sum(weights*results['jaccard']), 5)
347+
self._volume = np.sum(weights*self._vol_rois)
348+
284349
return runtime
285350

286351
def _list_outputs(self):
287352
outputs = self._outputs().get()
288-
for method in ("dice", "jaccard"):
289-
outputs[method] = getattr(self, '_' + method)
353+
outputs['labels'] = self._labels
354+
outputs['jaccard'] = self._jaccard
355+
outputs['dice'] = self._dice
290356
outputs['volume_difference'] = self._volume
357+
358+
outputs['roi_ji'] = self._ove_rois['jaccard'].tolist()
359+
outputs['roi_di'] = self._ove_rois['dice'].tolist()
360+
outputs['roi_voldiff'] = self._vol_rois.tolist()
291361
outputs['diff_file'] = os.path.abspath(self.inputs.out_file)
292362
return outputs
293363

294364

295365
class FuzzyOverlapInputSpec(BaseInterfaceInputSpec):
296366
in_ref = InputMultiPath( File(exists=True), mandatory=True,
297-
desc="Reference image. Requires the same dimensions as in_tst.")
367+
desc='Reference image. Requires the same dimensions as in_tst.')
298368
in_tst = InputMultiPath( File(exists=True), mandatory=True,
299-
desc="Test image. Requires the same dimensions as in_ref.")
300-
weighting = traits.Enum("none", "volume", "squared_vol", desc='""none": no class-overlap weighting is performed\
301-
"volume": computed class-overlaps are weighted by class volume\
302-
"squared_vol": computed class-overlaps are weighted by the squared volume of the class',usedefault=True)
303-
out_file = File("diff.nii", desc="alternative name for resulting difference-map", usedefault=True)
369+
desc='Test image. Requires the same dimensions as in_ref.')
370+
weighting = traits.Enum('none', 'volume', 'squared_vol', usedefault=True,
371+
desc=('\'none\': no class-overlap weighting is '
372+
'performed. \'volume\': computed class-'
373+
'overlaps are weighted by class volume '
374+
'\'squared_vol\': computed class-overlaps '
375+
'are weighted by the squared volume of '
376+
'the class'))
377+
out_file = File('diff.nii', desc='alternative name for resulting difference-map', usedefault=True)
304378

305379

306380
class FuzzyOverlapOutputSpec(TraitedSpec):
307-
jaccard = traits.Float( desc="Fuzzy Jaccard Index (fJI), all the classes" )
308-
dice = traits.Float( desc="Fuzzy Dice Index (fDI), all the classes" )
309-
diff_file = File(exists=True, desc="resulting difference-map of all classes, using the chosen weighting" )
310-
class_fji = traits.List( traits.Float(), desc="Array containing the fJIs of each computed class" )
311-
class_fdi = traits.List( traits.Float(), desc="Array containing the fDIs of each computed class" )
381+
jaccard = traits.Float( desc='Fuzzy Jaccard Index (fJI), all the classes' )
382+
dice = traits.Float( desc='Fuzzy Dice Index (fDI), all the classes' )
383+
diff_file = File(exists=True, desc='resulting difference-map of all classes, using the chosen weighting' )
384+
class_fji = traits.List( traits.Float(), desc='Array containing the fJIs of each computed class' )
385+
class_fdi = traits.List( traits.Float(), desc='Array containing the fDIs of each computed class' )
312386

313387

314388
class FuzzyOverlap(BaseInterface):

nipype/algorithms/misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1164,8 +1164,8 @@ def normalize_tpms(in_files, in_mask=None, out_files=[]):
11641164

11651165
return out_files
11661166

1167+
# Deprecated interfaces ------------------------------------------------------
11671168

1168-
# Deprecated interfaces ---------------------------------------------------------
11691169
class Distance(nam.Distance):
11701170
"""Calculates distance between two volumes.
11711171

nipype/algorithms/tests/test_auto_Overlap.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)