Skip to content

Commit 173d6f3

Browse files
committed
[algorithms] Overlap interface revision
* Now it accepts not only binary files, also ROI files with multiple labels. * Volumes can be handled in voxel units (as formerly) and mm3 * Keeps backwards compatibility of outputs and inputs
1 parent f626d94 commit 173d6f3

File tree

3 files changed

+133
-47
lines changed

3 files changed

+133
-47
lines changed

CHANGES

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Next Release
1818
AnalyzeWarp, PointsWarp, and EditTransform
1919
* ENH: New ANTs interface: ApplyTransformsToPoints
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: New data grabbing interface that works over SSH connections, SSHDataGrabber

nipype/algorithms/metrics.py

Lines changed: 118 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,104 @@ 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.abs(np.array(volumes1) - np.array(volumes2))
343+
344+
self._dice = round(np.sum(weights*results['dice']), 5)
345+
self._jaccard = round(np.sum(weights*results['jaccard']), 5)
346+
self._volume = np.sum(weights*self._vol_rois)
347+
284348
return runtime
285349

286350
def _list_outputs(self):
287351
outputs = self._outputs().get()
288-
for method in ("dice", "jaccard"):
289-
outputs[method] = getattr(self, '_' + method)
352+
outputs['labels'] = self._labels
353+
outputs['jaccard'] = self._jaccard
354+
outputs['dice'] = self._dice
290355
outputs['volume_difference'] = self._volume
356+
357+
outputs['roi_ji'] = self._ove_rois['jaccard'].tolist()
358+
outputs['roi_di'] = self._ove_rois['dice'].tolist()
359+
outputs['roi_voldiff'] = self._vol_rois.tolist()
291360
outputs['diff_file'] = os.path.abspath(self.inputs.out_file)
292361
return outputs
293362

294363

295364
class FuzzyOverlapInputSpec(BaseInterfaceInputSpec):
296365
in_ref = InputMultiPath( File(exists=True), mandatory=True,
297-
desc="Reference image. Requires the same dimensions as in_tst.")
366+
desc='Reference image. Requires the same dimensions as in_tst.')
298367
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)
368+
desc='Test image. Requires the same dimensions as in_ref.')
369+
weighting = traits.Enum('none', 'volume', 'squared_vol', usedefault=True,
370+
desc=('\'none\': no class-overlap weighting is '
371+
'performed. \'volume\': computed class-'
372+
'overlaps are weighted by class volume '
373+
'\'squared_vol\': computed class-overlaps '
374+
'are weighted by the squared volume of '
375+
'the class'))
376+
out_file = File('diff.nii', desc='alternative name for resulting difference-map', usedefault=True)
304377

305378

306379
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" )
380+
jaccard = traits.Float( desc='Fuzzy Jaccard Index (fJI), all the classes' )
381+
dice = traits.Float( desc='Fuzzy Dice Index (fDI), all the classes' )
382+
diff_file = File(exists=True, desc='resulting difference-map of all classes, using the chosen weighting' )
383+
class_fji = traits.List( traits.Float(), desc='Array containing the fJIs of each computed class' )
384+
class_fdi = traits.List( traits.Float(), desc='Array containing the fDIs of each computed class' )
312385

313386

314387
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()

0 commit comments

Comments
 (0)