3737iflogger = logging .getLogger ('interface' )
3838
3939
40-
4140class 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
217219class 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
227240class 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
234255class 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
295365class 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
306380class 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
314388class FuzzyOverlap (BaseInterface ):
0 commit comments