37
37
iflogger = logging .getLogger ('interface' )
38
38
39
39
40
-
41
40
class DistanceInputSpec (BaseInterfaceInputSpec ):
42
41
volume1 = File (exists = True , mandatory = True ,
43
42
desc = "Has to have the same dimensions as volume2." )
@@ -104,7 +103,10 @@ def _eucl_min(self, nii1, nii2):
104
103
dist_matrix = cdist (set1_coordinates .T , set2_coordinates .T )
105
104
(point1 , point2 ) = np .unravel_index (
106
105
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 , :])
108
110
109
111
def _eucl_cog (self , nii1 , nii2 ):
110
112
origdata1 = nii1 .get_data ().astype (np .bool )
@@ -216,38 +218,64 @@ def _list_outputs(self):
216
218
217
219
class OverlapInputSpec (BaseInterfaceInputSpec ):
218
220
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.' )
220
222
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' )
225
238
226
239
227
240
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' )
232
253
233
254
234
255
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.
236
264
237
265
Example
238
266
-------
239
267
240
268
>>> overlap = Overlap()
241
269
>>> overlap.inputs.volume1 = 'cont1.nii'
242
- >>> overlap.inputs.volume1 = 'cont2.nii'
270
+ >>> overlap.inputs.volume2 = 'cont2.nii'
243
271
>>> res = overlap.run() # doctest: +SKIP
244
- """
245
272
273
+ """
246
274
input_spec = OverlapInputSpec
247
275
output_spec = OverlapOutputSpec
248
276
249
277
def _bool_vec_dissimilarity (self , booldata1 , booldata2 , method ):
250
- methods = {" dice" : dice , " jaccard" : jaccard }
278
+ methods = {' dice' : dice , ' jaccard' : jaccard }
251
279
if not (np .any (booldata1 ) or np .any (booldata2 )):
252
280
return 0
253
281
return 1 - methods [method ](booldata1 .flat , booldata2 .flat )
@@ -256,59 +284,104 @@ def _run_interface(self, runtime):
256
284
nii1 = nb .load (self .inputs .volume1 )
257
285
nii2 = nb .load (self .inputs .volume2 )
258
286
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
263
288
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 ]
270
293
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 )
274
302
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 )
276
333
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
280
336
281
337
nb .save (nb .Nifti1Image (both_data , nii1 .get_affine (),
282
338
nii1 .get_header ()), self .inputs .out_file )
283
339
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
+
284
348
return runtime
285
349
286
350
def _list_outputs (self ):
287
351
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
290
355
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 ()
291
360
outputs ['diff_file' ] = os .path .abspath (self .inputs .out_file )
292
361
return outputs
293
362
294
363
295
364
class FuzzyOverlapInputSpec (BaseInterfaceInputSpec ):
296
365
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.' )
298
367
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 )
304
377
305
378
306
379
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' )
312
385
313
386
314
387
class FuzzyOverlap (BaseInterface ):
0 commit comments