Skip to content

Commit 0147bf5

Browse files
committed
Merge remote-tracking branch 'upstream/master' into fix/dipy
* upstream/master: typo in nipype/workflows/dmri/fsl/epi.py tiny typo Add tests another attempt at fixing bash conditionals fixed bash conditionals added no dep install version. we'll be testing on both. Added xor option in Camino connectivity for robustness Conmat test function Added more connectivity matrix options in Camino interface Fixed tests. remove deb packages installation fix: topup when out_base is a path [algorithms] Overlap interface revision
2 parents 45e4311 + efa7d39 commit 0147bf5

File tree

14 files changed

+233
-73
lines changed

14 files changed

+233
-73
lines changed

.travis.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@ language: python
44
python:
55
- 2.6
66
- 2.7
7+
env:
8+
- INSTALL_DEB_DEPENDECIES=true
9+
- INSTALL_DEB_DEPENDECIES=false
710
# Setup anaconda
811
before_install:
912
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.6.0-Linux-x86_64.sh -O miniconda.sh; fi
1013
- chmod +x miniconda.sh
1114
- ./miniconda.sh -b
1215
- export PATH=/home/travis/miniconda/bin:$PATH
13-
# The next couple lines fix a crash with multiprocessing on Travis
14-
- sudo rm -rf /dev/shm
15-
- sudo ln -s /run/shm /dev/shm
16-
- bash <(wget -q -O- http://neuro.debian.net/_files/neurodebian-travis.sh)
17-
- travis_retry sudo apt-get install -qq --no-install-recommends fsl afni elastix
18-
- travis_retry sudo apt-get install -qq fsl-atlases
19-
- source /etc/fsl/fsl.sh
16+
- if $INSTALL_DEB_DEPENDECIES; then sudo rm -rf /dev/shm; fi
17+
- if $INSTALL_DEB_DEPENDECIES; then sudo ln -s /run/shm /dev/shm; fi
18+
- if $INSTALL_DEB_DEPENDECIES; then bash <(wget -q -O- http://neuro.debian.net/_files/neurodebian-travis.sh); fi
19+
- if $INSTALL_DEB_DEPENDECIES; then travis_retry sudo apt-get install -qq --no-install-recommends fsl afni elastix; fi
20+
- if $INSTALL_DEB_DEPENDECIES; then travis_retry sudo apt-get install -qq fsl-atlases; fi
21+
- if $INSTALL_DEB_DEPENDECIES; then source /etc/fsl/fsl.sh; fi
2022

2123
# Install packages
2224
install:

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

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/tests/test_auto_Overlap.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@
33
from nipype.algorithms.misc import Overlap
44

55
def test_Overlap_inputs():
6-
input_map = dict(ignore_exception=dict(nohash=True,
6+
input_map = dict(bg_overlap=dict(mandatory=True,
7+
usedefault=True,
8+
),
9+
ignore_exception=dict(nohash=True,
710
usedefault=True,
811
),
912
mask_volume=dict(),
1013
out_file=dict(usedefault=True,
1114
),
15+
vol_units=dict(mandatory=True,
16+
usedefault=True,
17+
),
1218
volume1=dict(mandatory=True,
1319
),
1420
volume2=dict(mandatory=True,
1521
),
22+
weighting=dict(usedefault=True,
23+
),
1624
)
1725
inputs = Overlap.input_spec()
1826

@@ -24,6 +32,10 @@ def test_Overlap_outputs():
2432
output_map = dict(dice=dict(),
2533
diff_file=dict(),
2634
jaccard=dict(),
35+
labels=dict(),
36+
roi_di=dict(),
37+
roi_ji=dict(),
38+
roi_voldiff=dict(),
2739
volume_difference=dict(),
2840
)
2941
outputs = Overlap.output_spec()
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
4+
# vi: set ft=python sts=4 ts=4 sw=4 et:
5+
6+
import os
7+
from shutil import rmtree
8+
from tempfile import mkdtemp
9+
10+
from nipype.testing import (assert_equal, assert_raises,
11+
assert_almost_equal, example_data)
12+
13+
import numpy as np
14+
import nibabel as nb
15+
16+
17+
def test_overlap():
18+
from nipype.algorithms.metrics import Overlap
19+
20+
def check_close(val1, val2):
21+
import numpy.testing as npt
22+
return npt.assert_almost_equal(val1, val2, decimal=3)
23+
24+
tempdir = mkdtemp()
25+
in1 = example_data('segmentation0.nii.gz')
26+
in2 = example_data('segmentation1.nii.gz')
27+
28+
os.chdir(tempdir)
29+
overlap = Overlap()
30+
overlap.inputs.volume1 = in1
31+
overlap.inputs.volume2 = in1
32+
res = overlap.run()
33+
yield check_close, res.outputs.jaccard, 1.0
34+
overlap = Overlap()
35+
overlap.inputs.volume1 = in1
36+
overlap.inputs.volume2 = in2
37+
res = overlap.run()
38+
39+
yield check_close, res.outputs.jaccard, 0.99705
40+
41+
overlap = Overlap()
42+
overlap.inputs.volume1 = in1
43+
overlap.inputs.volume2 = in2
44+
overlap.inputs.vol_units = 'mm'
45+
res = overlap.run()
46+
47+
yield check_close, res.outputs.jaccard, 0.99705
48+
yield (check_close, res.outputs.roi_voldiff,
49+
np.array([0.0063086, -0.0025506, 0.0]))
50+
51+
rmtree(tempdir)

0 commit comments

Comments
 (0)