From db511b6e0d384b87ccff4c5bd864b9a9ce61eea9 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 3 Apr 2014 14:59:04 +0200 Subject: [PATCH 01/15] Evaluation altorithms separated from misc Created a new nipype.algorithms.eval group for methods performing simple error, distance or similitude computations over images. I think this is better than having them mixed up with, e.g., nifti or csv files management. I also add a new interface: ErrorMap, that computes a distance function between two images (e.g. two displacement fields). Several metrics will be available, however now I contribute only with squared differences maps. --- nipype/algorithms/eval.py | 483 ++++++++++++++++++++++++++++++++++++++ nipype/algorithms/misc.py | 367 ----------------------------- 2 files changed, 483 insertions(+), 367 deletions(-) create mode 100644 nipype/algorithms/eval.py diff --git a/nipype/algorithms/eval.py b/nipype/algorithms/eval.py new file mode 100644 index 0000000000..89ef148baf --- /dev/null +++ b/nipype/algorithms/eval.py @@ -0,0 +1,483 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +''' +Image assessment algorithms. Typical overlap and error computation +measures to evaluate results from other processing units. + + Change directory to provide relative paths for doctests + >>> import os + >>> filepath = os.path.dirname( os.path.realpath( __file__ ) ) + >>> datadir = os.path.realpath(os.path.join(filepath, '../testing/data')) + >>> os.chdir(datadir) + +''' + +import os +import os.path as op + +import nibabel as nb +import numpy as np +from math import floor, ceil +from scipy.ndimage.morphology import grey_dilation +from scipy.ndimage.morphology import binary_erosion +from scipy.spatial.distance import cdist, euclidean, dice, jaccard +from scipy.ndimage.measurements import center_of_mass, label +from scipy.special import legendre +import scipy.io as sio +import itertools +import scipy.stats as stats + +from .. import logging + +from ..interfaces.base import (BaseInterface, traits, TraitedSpec, File, + InputMultiPath, OutputMultiPath, + BaseInterfaceInputSpec, isdefined) +from ..utils.filemanip import fname_presuffix, split_filename +iflogger = logging.getLogger('interface') + + + +class DistanceInputSpec(BaseInterfaceInputSpec): + volume1 = File(exists=True, mandatory=True, + desc="Has to have the same dimensions as volume2.") + volume2 = File( + exists=True, mandatory=True, + desc="Has to have the same dimensions as volume1." + ) + method = traits.Enum( + "eucl_min", "eucl_cog", "eucl_mean", "eucl_wmean", "eucl_max", + desc='""eucl_min": Euclidean distance between two closest points\ + "eucl_cog": mean Euclidian distance between the Center of Gravity\ + of volume1 and CoGs of volume2\ + "eucl_mean": mean Euclidian minimum distance of all volume2 voxels\ + to volume1\ + "eucl_wmean": mean Euclidian minimum distance of all volume2 voxels\ + to volume1 weighted by their values\ + "eucl_max": maximum over minimum Euclidian distances of all volume2\ + voxels to volume1 (also known as the Hausdorff distance)', + usedefault=True + ) + mask_volume = File( + exists=True, desc="calculate overlap only within this mask.") + + +class DistanceOutputSpec(TraitedSpec): + distance = traits.Float() + point1 = traits.Array(shape=(3,)) + point2 = traits.Array(shape=(3,)) + histogram = File() + + +class Distance(BaseInterface): + ''' + Calculates distance between two volumes. + ''' + input_spec = DistanceInputSpec + output_spec = DistanceOutputSpec + + _hist_filename = "hist.pdf" + + def _find_border(self, data): + eroded = binary_erosion(data) + border = np.logical_and(data, np.logical_not(eroded)) + return border + + def _get_coordinates(self, data, affine): + if len(data.shape) == 4: + data = data[:, :, :, 0] + indices = np.vstack(np.nonzero(data)) + indices = np.vstack((indices, np.ones(indices.shape[1]))) + coordinates = np.dot(affine, indices) + return coordinates[:3, :] + + def _eucl_min(self, nii1, nii2): + origdata1 = nii1.get_data().astype(np.bool) + border1 = self._find_border(origdata1) + + origdata2 = nii2.get_data().astype(np.bool) + border2 = self._find_border(origdata2) + + set1_coordinates = self._get_coordinates(border1, nii1.get_affine()) + + set2_coordinates = self._get_coordinates(border2, nii2.get_affine()) + + dist_matrix = cdist(set1_coordinates.T, set2_coordinates.T) + (point1, point2) = np.unravel_index( + np.argmin(dist_matrix), dist_matrix.shape) + return (euclidean(set1_coordinates.T[point1, :], set2_coordinates.T[point2, :]), set1_coordinates.T[point1, :], set2_coordinates.T[point2, :]) + + def _eucl_cog(self, nii1, nii2): + origdata1 = nii1.get_data().astype(np.bool) + cog_t = np.array(center_of_mass(origdata1)).reshape(-1, 1) + cog_t = np.vstack((cog_t, np.array([1]))) + cog_t_coor = np.dot(nii1.get_affine(), cog_t)[:3, :] + + origdata2 = nii2.get_data().astype(np.bool) + (labeled_data, n_labels) = label(origdata2) + + cogs = np.ones((4, n_labels)) + + for i in range(n_labels): + cogs[:3, i] = np.array(center_of_mass(origdata2, + labeled_data, i + 1)) + + cogs_coor = np.dot(nii2.get_affine(), cogs)[:3, :] + + dist_matrix = cdist(cog_t_coor.T, cogs_coor.T) + + return np.mean(dist_matrix) + + def _eucl_mean(self, nii1, nii2, weighted=False): + origdata1 = nii1.get_data().astype(np.bool) + border1 = self._find_border(origdata1) + + origdata2 = nii2.get_data().astype(np.bool) + + set1_coordinates = self._get_coordinates(border1, nii1.get_affine()) + set2_coordinates = self._get_coordinates(origdata2, nii2.get_affine()) + + dist_matrix = cdist(set1_coordinates.T, set2_coordinates.T) + min_dist_matrix = np.amin(dist_matrix, axis=0) + import matplotlib.pyplot as plt + plt.figure() + plt.hist(min_dist_matrix, 50, normed=1, facecolor='green') + plt.savefig(self._hist_filename) + plt.clf() + plt.close() + + if weighted: + return np.average( + min_dist_matrix, + weights=nii2.get_data()[origdata2].flat + ) + else: + return np.mean(min_dist_matrix) + + def _eucl_max(self, nii1, nii2): + origdata1 = nii1.get_data() + origdata1 = np.logical_not( + np.logical_or(origdata1 == 0, np.isnan(origdata1))) + origdata2 = nii2.get_data() + origdata2 = np.logical_not( + np.logical_or(origdata2 == 0, np.isnan(origdata2))) + + if isdefined(self.inputs.mask_volume): + maskdata = nb.load(self.inputs.mask_volume).get_data() + maskdata = np.logical_not( + np.logical_or(maskdata == 0, np.isnan(maskdata))) + origdata1 = np.logical_and(maskdata, origdata1) + origdata2 = np.logical_and(maskdata, origdata2) + + if origdata1.max() == 0 or origdata2.max() == 0: + return np.NaN + + border1 = self._find_border(origdata1) + border2 = self._find_border(origdata2) + + set1_coordinates = self._get_coordinates(border1, nii1.get_affine()) + set2_coordinates = self._get_coordinates(border2, nii2.get_affine()) + distances = cdist(set1_coordinates.T, set2_coordinates.T) + mins = np.concatenate( + (np.amin(distances, axis=0), np.amin(distances, axis=1))) + + return np.max(mins) + + def _run_interface(self, runtime): + nii1 = nb.load(self.inputs.volume1) + nii2 = nb.load(self.inputs.volume2) + + if self.inputs.method == "eucl_min": + self._distance, self._point1, self._point2 = self._eucl_min( + nii1, nii2) + + elif self.inputs.method == "eucl_cog": + self._distance = self._eucl_cog(nii1, nii2) + + elif self.inputs.method == "eucl_mean": + self._distance = self._eucl_mean(nii1, nii2) + + elif self.inputs.method == "eucl_wmean": + self._distance = self._eucl_mean(nii1, nii2, weighted=True) + elif self.inputs.method == "eucl_max": + self._distance = self._eucl_max(nii1, nii2) + + return runtime + + def _list_outputs(self): + outputs = self._outputs().get() + outputs['distance'] = self._distance + if self.inputs.method == "eucl_min": + outputs['point1'] = self._point1 + outputs['point2'] = self._point2 + elif self.inputs.method in ["eucl_mean", "eucl_wmean"]: + outputs['histogram'] = os.path.abspath(self._hist_filename) + return outputs + + +class OverlapInputSpec(BaseInterfaceInputSpec): + volume1 = File(exists=True, mandatory=True, + desc="Has to have the same dimensions as volume2.") + volume2 = File(exists=True, mandatory=True, + desc="Has to have the same dimensions as volume1.") + mask_volume = File( + exists=True, desc="calculate overlap only within this mask.") + out_file = File("diff.nii", usedefault=True) + + +class OverlapOutputSpec(TraitedSpec): + jaccard = traits.Float() + dice = traits.Float() + volume_difference = traits.Int() + diff_file = File(exists=True) + + +class Overlap(BaseInterface): + """ + Calculates various overlap measures between two maps. + + Example + ------- + + >>> overlap = Overlap() + >>> overlap.inputs.volume1 = 'cont1.nii' + >>> overlap.inputs.volume1 = 'cont2.nii' + >>> res = overlap.run() # doctest: +SKIP + """ + + input_spec = OverlapInputSpec + output_spec = OverlapOutputSpec + + def _bool_vec_dissimilarity(self, booldata1, booldata2, method): + methods = {"dice": dice, "jaccard": jaccard} + if not (np.any(booldata1) or np.any(booldata2)): + return 0 + return 1 - methods[method](booldata1.flat, booldata2.flat) + + def _run_interface(self, runtime): + nii1 = nb.load(self.inputs.volume1) + nii2 = nb.load(self.inputs.volume2) + + origdata1 = np.logical_not( + np.logical_or(nii1.get_data() == 0, np.isnan(nii1.get_data()))) + origdata2 = np.logical_not( + np.logical_or(nii2.get_data() == 0, np.isnan(nii2.get_data()))) + + if isdefined(self.inputs.mask_volume): + maskdata = nb.load(self.inputs.mask_volume).get_data() + maskdata = np.logical_not( + np.logical_or(maskdata == 0, np.isnan(maskdata))) + origdata1 = np.logical_and(maskdata, origdata1) + origdata2 = np.logical_and(maskdata, origdata2) + + for method in ("dice", "jaccard"): + setattr(self, '_' + method, self._bool_vec_dissimilarity( + origdata1, origdata2, method=method)) + + self._volume = int(origdata1.sum() - origdata2.sum()) + + both_data = np.zeros(origdata1.shape) + both_data[origdata1] = 1 + both_data[origdata2] += 2 + + nb.save(nb.Nifti1Image(both_data, nii1.get_affine(), + nii1.get_header()), self.inputs.out_file) + + return runtime + + def _list_outputs(self): + outputs = self._outputs().get() + for method in ("dice", "jaccard"): + outputs[method] = getattr(self, '_' + method) + outputs['volume_difference'] = self._volume + outputs['diff_file'] = os.path.abspath(self.inputs.out_file) + return outputs + + +class FuzzyOverlapInputSpec(BaseInterfaceInputSpec): + in_ref = InputMultiPath( File(exists=True), mandatory=True, + desc="Reference image. Requires the same dimensions as in_tst.") + in_tst = InputMultiPath( File(exists=True), mandatory=True, + desc="Test image. Requires the same dimensions as in_ref.") + weighting = traits.Enum("none", "volume", "squared_vol", desc='""none": no class-overlap weighting is performed\ + "volume": computed class-overlaps are weighted by class volume\ + "squared_vol": computed class-overlaps are weighted by the squared volume of the class',usedefault=True) + out_file = File("diff.nii", desc="alternative name for resulting difference-map", usedefault=True) + + +class FuzzyOverlapOutputSpec(TraitedSpec): + jaccard = traits.Float( desc="Fuzzy Jaccard Index (fJI), all the classes" ) + dice = traits.Float( desc="Fuzzy Dice Index (fDI), all the classes" ) + diff_file = File(exists=True, desc="resulting difference-map of all classes, using the chosen weighting" ) + class_fji = traits.List( traits.Float(), desc="Array containing the fJIs of each computed class" ) + class_fdi = traits.List( traits.Float(), desc="Array containing the fDIs of each computed class" ) + + +class FuzzyOverlap(BaseInterface): + """ Calculates various overlap measures between two maps, using the fuzzy + definition proposed in: Crum et al., Generalized Overlap Measures for + Evaluation and Validation in Medical Image Analysis, IEEE Trans. Med. + Ima. 25(11),pp 1451-1461, Nov. 2006. + + in_ref and in_tst are lists of 2/3D images, each element on the list + containing one volume fraction map of a class in a fuzzy partition + of the domain. + + Example + ------- + + >>> overlap = FuzzyOverlap() + >>> overlap.inputs.in_ref = [ 'ref_class0.nii', 'ref_class1.nii' ] + >>> overlap.inputs.in_tst = [ 'tst_class0.nii', 'tst_class1.nii' ] + >>> overlap.inputs.weighting = 'volume' + >>> res = overlap.run() # doctest: +SKIP + """ + + input_spec = FuzzyOverlapInputSpec + output_spec = FuzzyOverlapOutputSpec + + def _run_interface(self, runtime): + ncomp = len(self.inputs.in_ref) + assert( ncomp == len(self.inputs.in_tst) ) + weights = np.ones( shape=ncomp ) + + img_ref = np.array( [ nb.load( fname ).get_data() for fname in self.inputs.in_ref ] ) + img_tst = np.array( [ nb.load( fname ).get_data() for fname in self.inputs.in_tst ] ) + + + msk = np.sum(img_ref, axis=0) + msk[msk>0] = 1.0 + tst_msk = np.sum(img_tst, axis=0) + tst_msk[tst_msk>0] = 1.0 + + #check that volumes are normalized + #img_ref[:][msk>0] = img_ref[:][msk>0] / (np.sum( img_ref, axis=0 ))[msk>0] + #img_tst[tst_msk>0] = img_tst[tst_msk>0] / np.sum( img_tst, axis=0 )[tst_msk>0] + + self._jaccards = [] + volumes = [] + + diff_im = np.zeros( img_ref.shape ) + + for ref_comp, tst_comp, diff_comp in zip( img_ref, img_tst, diff_im ): + num = np.minimum( ref_comp, tst_comp ) + ddr = np.maximum( ref_comp, tst_comp ) + diff_comp[ddr>0]+= 1.0-(num[ddr>0]/ddr[ddr>0]) + self._jaccards.append( np.sum( num ) / np.sum( ddr ) ) + volumes.append( np.sum( ref_comp ) ) + + self._dices = 2.0*np.array(self._jaccards) / (np.array(self._jaccards) +1.0 ) + + if self.inputs.weighting != "none": + weights = 1.0 / np.array(volumes) + if self.inputs.weighting == "squared_vol": + weights = weights**2 + + weights = weights / np.sum( weights ) + + setattr( self, '_jaccard', np.sum( weights * self._jaccards ) ) + setattr( self, '_dice', np.sum( weights * self._dices ) ) + + + diff = np.zeros( diff_im[0].shape ) + + for w,ch in zip(weights,diff_im): + ch[msk==0] = 0 + diff+= w* ch + + nb.save(nb.Nifti1Image(diff, nb.load( self.inputs.in_ref[0]).get_affine(), + nb.load( self.inputs.in_ref[0]).get_header()), self.inputs.out_file ) + + + return runtime + + def _list_outputs(self): + outputs = self._outputs().get() + for method in ("dice", "jaccard"): + outputs[method] = getattr(self, '_' + method) + #outputs['volume_difference'] = self._volume + outputs['diff_file'] = os.path.abspath(self.inputs.out_file) + outputs['class_fji'] = np.array(self._jaccards).astype(float).tolist(); + outputs['class_fdi']= self._dices.astype(float).tolist(); + return outputs + + +class ErrorMapInputSpec( BaseInterfaceInputSpec ): + in_ref = File(exists=True, mandatory=True, + desc="Reference image. Requires the same dimensions as in_tst.") + in_tst = File(exists=True, mandatory=True, + desc="Test image. Requires the same dimensions as in_ref.") + method = traits.Enum( "squared_diff", "eucl", + desc='', + usedefault=True ) + out_map = File( desc="Name for the output file" ) + +class ErrorMapOutputSpec(TraitedSpec): + out_map = File(exists=True, desc="resulting error map" ) + + + +class ErrorMap(BaseInterface): + """ Calculates the error (distance) map between two input volumes. + + Example + ------- + + >>> errormap = ErrorMap() + >>> errormap.inputs.in_ref = 'cont1.nii' + >>> errormap.inputs.in_tst = 'cont2.nii' + >>> res = errormap.run() # doctest: +SKIP + """ + input_spec = ErrorMapInputSpec + output_spec = ErrorMapOutputSpec + _out_file = "" + + + def _run_interface( self, runtime ): + nii_ref = nb.load( self.inputs.in_ref ) + ref_data = nii_ref.get_data() + tst_data = nb.load( self.inputs.in_tst ).get_data() + + assert( ref_data.ndim == tst_data.ndim ) + + if ( ref_data.ndim == 4 ): + comps = ref_data.shape[-1] + mapshape = ref_data.shape[:-1] + refvector = np.reshape( ref_data, (-1,comps)) + tstvector = np.reshape( tst_data, (-1,comps)) + else: + mapshape = ref_data.shape + refvector = ref_data.reshape(-1) + tstvector = tst_data.reshape(-1) + + diffvector = (tstvector-refvector)**2 + if ( ref_data.ndim > 1 ): + diffvector = np.sum( diffvector, axis=1 ) + + diffmap = diffvector.reshape( mapshape ) + + hdr = nii_ref.get_header().copy() + hdr.set_data_dtype( np.float32 ) + hdr['data_type'] = 16 + hdr.set_data_shape( diffmap.shape ) + + niimap = nb.Nifti1Image( diffmap.astype( np.float32 ), + nii_ref.get_affine(), hdr ) + + if not isdefined( self.inputs.out_map ): + fname,ext = op.splitext( op.basename( self.inputs.in_tst ) ) + if ext=='.gz': + fname,ext2 = op.splitext( fname ) + ext = ext + ext2 + self._out_file = op.abspath( fname + "_errmap" + ext ) + else: + self._out_file = self.inputs.out_map + + nb.save( niimap, self._out_file ) + + return runtime + + def _list_outputs(self): + outputs = self.output_spec().get() + outputs['out_map'] = self._out_file + + return outputs diff --git a/nipype/algorithms/misc.py b/nipype/algorithms/misc.py index ff6c3d6cb0..3c144f77f1 100644 --- a/nipype/algorithms/misc.py +++ b/nipype/algorithms/misc.py @@ -210,373 +210,6 @@ def _list_outputs(self): self._gen_output_filename(fname)) return outputs - -class DistanceInputSpec(BaseInterfaceInputSpec): - volume1 = File(exists=True, mandatory=True, - desc="Has to have the same dimensions as volume2.") - volume2 = File( - exists=True, mandatory=True, - desc="Has to have the same dimensions as volume1." - ) - method = traits.Enum( - "eucl_min", "eucl_cog", "eucl_mean", "eucl_wmean", "eucl_max", - desc='""eucl_min": Euclidean distance between two closest points\ - "eucl_cog": mean Euclidian distance between the Center of Gravity\ - of volume1 and CoGs of volume2\ - "eucl_mean": mean Euclidian minimum distance of all volume2 voxels\ - to volume1\ - "eucl_wmean": mean Euclidian minimum distance of all volume2 voxels\ - to volume1 weighted by their values\ - "eucl_max": maximum over minimum Euclidian distances of all volume2\ - voxels to volume1 (also known as the Hausdorff distance)', - usedefault=True - ) - mask_volume = File( - exists=True, desc="calculate overlap only within this mask.") - - -class DistanceOutputSpec(TraitedSpec): - distance = traits.Float() - point1 = traits.Array(shape=(3,)) - point2 = traits.Array(shape=(3,)) - histogram = File() - - -class Distance(BaseInterface): - ''' - Calculates distance between two volumes. - ''' - input_spec = DistanceInputSpec - output_spec = DistanceOutputSpec - - _hist_filename = "hist.pdf" - - def _find_border(self, data): - eroded = binary_erosion(data) - border = np.logical_and(data, np.logical_not(eroded)) - return border - - def _get_coordinates(self, data, affine): - if len(data.shape) == 4: - data = data[:, :, :, 0] - indices = np.vstack(np.nonzero(data)) - indices = np.vstack((indices, np.ones(indices.shape[1]))) - coordinates = np.dot(affine, indices) - return coordinates[:3, :] - - def _eucl_min(self, nii1, nii2): - origdata1 = nii1.get_data().astype(np.bool) - border1 = self._find_border(origdata1) - - origdata2 = nii2.get_data().astype(np.bool) - border2 = self._find_border(origdata2) - - set1_coordinates = self._get_coordinates(border1, nii1.get_affine()) - - set2_coordinates = self._get_coordinates(border2, nii2.get_affine()) - - dist_matrix = cdist(set1_coordinates.T, set2_coordinates.T) - (point1, point2) = np.unravel_index( - np.argmin(dist_matrix), dist_matrix.shape) - return (euclidean(set1_coordinates.T[point1, :], set2_coordinates.T[point2, :]), set1_coordinates.T[point1, :], set2_coordinates.T[point2, :]) - - def _eucl_cog(self, nii1, nii2): - origdata1 = nii1.get_data().astype(np.bool) - cog_t = np.array(center_of_mass(origdata1)).reshape(-1, 1) - cog_t = np.vstack((cog_t, np.array([1]))) - cog_t_coor = np.dot(nii1.get_affine(), cog_t)[:3, :] - - origdata2 = nii2.get_data().astype(np.bool) - (labeled_data, n_labels) = label(origdata2) - - cogs = np.ones((4, n_labels)) - - for i in range(n_labels): - cogs[:3, i] = np.array(center_of_mass(origdata2, - labeled_data, i + 1)) - - cogs_coor = np.dot(nii2.get_affine(), cogs)[:3, :] - - dist_matrix = cdist(cog_t_coor.T, cogs_coor.T) - - return np.mean(dist_matrix) - - def _eucl_mean(self, nii1, nii2, weighted=False): - origdata1 = nii1.get_data().astype(np.bool) - border1 = self._find_border(origdata1) - - origdata2 = nii2.get_data().astype(np.bool) - - set1_coordinates = self._get_coordinates(border1, nii1.get_affine()) - set2_coordinates = self._get_coordinates(origdata2, nii2.get_affine()) - - dist_matrix = cdist(set1_coordinates.T, set2_coordinates.T) - min_dist_matrix = np.amin(dist_matrix, axis=0) - import matplotlib.pyplot as plt - plt.figure() - plt.hist(min_dist_matrix, 50, normed=1, facecolor='green') - plt.savefig(self._hist_filename) - plt.clf() - plt.close() - - if weighted: - return np.average( - min_dist_matrix, - weights=nii2.get_data()[origdata2].flat - ) - else: - return np.mean(min_dist_matrix) - - def _eucl_max(self, nii1, nii2): - origdata1 = nii1.get_data() - origdata1 = np.logical_not( - np.logical_or(origdata1 == 0, np.isnan(origdata1))) - origdata2 = nii2.get_data() - origdata2 = np.logical_not( - np.logical_or(origdata2 == 0, np.isnan(origdata2))) - - if isdefined(self.inputs.mask_volume): - maskdata = nb.load(self.inputs.mask_volume).get_data() - maskdata = np.logical_not( - np.logical_or(maskdata == 0, np.isnan(maskdata))) - origdata1 = np.logical_and(maskdata, origdata1) - origdata2 = np.logical_and(maskdata, origdata2) - - if origdata1.max() == 0 or origdata2.max() == 0: - return np.NaN - - border1 = self._find_border(origdata1) - border2 = self._find_border(origdata2) - - set1_coordinates = self._get_coordinates(border1, nii1.get_affine()) - set2_coordinates = self._get_coordinates(border2, nii2.get_affine()) - distances = cdist(set1_coordinates.T, set2_coordinates.T) - mins = np.concatenate( - (np.amin(distances, axis=0), np.amin(distances, axis=1))) - - return np.max(mins) - - def _run_interface(self, runtime): - nii1 = nb.load(self.inputs.volume1) - nii2 = nb.load(self.inputs.volume2) - - if self.inputs.method == "eucl_min": - self._distance, self._point1, self._point2 = self._eucl_min( - nii1, nii2) - - elif self.inputs.method == "eucl_cog": - self._distance = self._eucl_cog(nii1, nii2) - - elif self.inputs.method == "eucl_mean": - self._distance = self._eucl_mean(nii1, nii2) - - elif self.inputs.method == "eucl_wmean": - self._distance = self._eucl_mean(nii1, nii2, weighted=True) - elif self.inputs.method == "eucl_max": - self._distance = self._eucl_max(nii1, nii2) - - return runtime - - def _list_outputs(self): - outputs = self._outputs().get() - outputs['distance'] = self._distance - if self.inputs.method == "eucl_min": - outputs['point1'] = self._point1 - outputs['point2'] = self._point2 - elif self.inputs.method in ["eucl_mean", "eucl_wmean"]: - outputs['histogram'] = os.path.abspath(self._hist_filename) - return outputs - - -class OverlapInputSpec(BaseInterfaceInputSpec): - volume1 = File(exists=True, mandatory=True, - desc="Has to have the same dimensions as volume2.") - volume2 = File(exists=True, mandatory=True, - desc="Has to have the same dimensions as volume1.") - mask_volume = File( - exists=True, desc="calculate overlap only within this mask.") - out_file = File("diff.nii", usedefault=True) - - -class OverlapOutputSpec(TraitedSpec): - jaccard = traits.Float() - dice = traits.Float() - volume_difference = traits.Int() - diff_file = File(exists=True) - - -class Overlap(BaseInterface): - """ - Calculates various overlap measures between two maps. - - Example - ------- - - >>> overlap = Overlap() - >>> overlap.inputs.volume1 = 'cont1.nii' - >>> overlap.inputs.volume1 = 'cont2.nii' - >>> res = overlap.run() # doctest: +SKIP - """ - - input_spec = OverlapInputSpec - output_spec = OverlapOutputSpec - - def _bool_vec_dissimilarity(self, booldata1, booldata2, method): - methods = {"dice": dice, "jaccard": jaccard} - if not (np.any(booldata1) or np.any(booldata2)): - return 0 - return 1 - methods[method](booldata1.flat, booldata2.flat) - - def _run_interface(self, runtime): - nii1 = nb.load(self.inputs.volume1) - nii2 = nb.load(self.inputs.volume2) - - origdata1 = np.logical_not( - np.logical_or(nii1.get_data() == 0, np.isnan(nii1.get_data()))) - origdata2 = np.logical_not( - np.logical_or(nii2.get_data() == 0, np.isnan(nii2.get_data()))) - - if isdefined(self.inputs.mask_volume): - maskdata = nb.load(self.inputs.mask_volume).get_data() - maskdata = np.logical_not( - np.logical_or(maskdata == 0, np.isnan(maskdata))) - origdata1 = np.logical_and(maskdata, origdata1) - origdata2 = np.logical_and(maskdata, origdata2) - - for method in ("dice", "jaccard"): - setattr(self, '_' + method, self._bool_vec_dissimilarity( - origdata1, origdata2, method=method)) - - self._volume = int(origdata1.sum() - origdata2.sum()) - - both_data = np.zeros(origdata1.shape) - both_data[origdata1] = 1 - both_data[origdata2] += 2 - - nb.save(nb.Nifti1Image(both_data, nii1.get_affine(), - nii1.get_header()), self.inputs.out_file) - - return runtime - - def _list_outputs(self): - outputs = self._outputs().get() - for method in ("dice", "jaccard"): - outputs[method] = getattr(self, '_' + method) - outputs['volume_difference'] = self._volume - outputs['diff_file'] = os.path.abspath(self.inputs.out_file) - return outputs - - -class FuzzyOverlapInputSpec(BaseInterfaceInputSpec): - in_ref = InputMultiPath( File(exists=True), mandatory=True, - desc="Reference image. Requires the same dimensions as in_tst.") - in_tst = InputMultiPath( File(exists=True), mandatory=True, - desc="Test image. Requires the same dimensions as in_ref.") - weighting = traits.Enum("none", "volume", "squared_vol", desc='""none": no class-overlap weighting is performed\ - "volume": computed class-overlaps are weighted by class volume\ - "squared_vol": computed class-overlaps are weighted by the squared volume of the class',usedefault=True) - out_file = File("diff.nii", desc="alternative name for resulting difference-map", usedefault=True) - - -class FuzzyOverlapOutputSpec(TraitedSpec): - jaccard = traits.Float( desc="Fuzzy Jaccard Index (fJI), all the classes" ) - dice = traits.Float( desc="Fuzzy Dice Index (fDI), all the classes" ) - diff_file = File(exists=True, desc="resulting difference-map of all classes, using the chosen weighting" ) - class_fji = traits.List( traits.Float(), desc="Array containing the fJIs of each computed class" ) - class_fdi = traits.List( traits.Float(), desc="Array containing the fDIs of each computed class" ) - - -class FuzzyOverlap(BaseInterface): - """ - Calculates various overlap measures between two maps, using the fuzzy - definition proposed in: Crum et al., Generalized Overlap Measures for - Evaluation and Validation in Medical Image Analysis, IEEE Trans. Med. - Ima. 25(11),pp 1451-1461, Nov. 2006. - - in_ref and in_tst are lists of 2/3D images, each element on the list - containing one volume fraction map of a class in a fuzzy partition - of the domain. - - Example - ------- - - >>> overlap = FuzzyOverlap() - >>> overlap.inputs.in_ref = [ 'ref_class0.nii', 'ref_class1.nii' ] - >>> overlap.inputs.in_tst = [ 'tst_class0.nii', 'tst_class1.nii' ] - >>> overlap.inputs.weighting = 'volume' - >>> res = overlap.run() # doctest: +SKIP - """ - - input_spec = FuzzyOverlapInputSpec - output_spec = FuzzyOverlapOutputSpec - - def _run_interface(self, runtime): - ncomp = len(self.inputs.in_ref) - assert( ncomp == len(self.inputs.in_tst) ) - weights = np.ones( shape=ncomp ) - - img_ref = np.array( [ nb.load( fname ).get_data() for fname in self.inputs.in_ref ] ) - img_tst = np.array( [ nb.load( fname ).get_data() for fname in self.inputs.in_tst ] ) - - - msk = np.sum(img_ref, axis=0) - msk[msk>0] = 1.0 - tst_msk = np.sum(img_tst, axis=0) - tst_msk[tst_msk>0] = 1.0 - - #check that volumes are normalized - #img_ref[:][msk>0] = img_ref[:][msk>0] / (np.sum( img_ref, axis=0 ))[msk>0] - #img_tst[tst_msk>0] = img_tst[tst_msk>0] / np.sum( img_tst, axis=0 )[tst_msk>0] - - self._jaccards = [] - volumes = [] - - diff_im = np.zeros( img_ref.shape ) - - for ref_comp, tst_comp, diff_comp in zip( img_ref, img_tst, diff_im ): - num = np.minimum( ref_comp, tst_comp ) - ddr = np.maximum( ref_comp, tst_comp ) - diff_comp[ddr>0]+= 1.0-(num[ddr>0]/ddr[ddr>0]) - self._jaccards.append( np.sum( num ) / np.sum( ddr ) ) - volumes.append( np.sum( ref_comp ) ) - - self._dices = 2.0*np.array(self._jaccards) / (np.array(self._jaccards) +1.0 ) - - if self.inputs.weighting != "none": - weights = 1.0 / np.array(volumes) - if self.inputs.weighting == "squared_vol": - weights = weights**2 - - weights = weights / np.sum( weights ) - - setattr( self, '_jaccard', np.sum( weights * self._jaccards ) ) - setattr( self, '_dice', np.sum( weights * self._dices ) ) - - - diff = np.zeros( diff_im[0].shape ) - - for w,ch in zip(weights,diff_im): - ch[msk==0] = 0 - diff+= w* ch - - nb.save(nb.Nifti1Image(diff, nb.load( self.inputs.in_ref[0]).get_affine(), - nb.load( self.inputs.in_ref[0]).get_header()), self.inputs.out_file ) - - - return runtime - - def _list_outputs(self): - outputs = self._outputs().get() - for method in ("dice", "jaccard"): - outputs[method] = getattr(self, '_' + method) - #outputs['volume_difference'] = self._volume - outputs['diff_file'] = os.path.abspath(self.inputs.out_file) - outputs['class_fji'] = np.array(self._jaccards).astype(float).tolist(); - outputs['class_fdi']= self._dices.astype(float).tolist(); - return outputs - - - class CreateNiftiInputSpec(BaseInterfaceInputSpec): data_file = File(exists=True, mandatory=True, desc="ANALYZE img file") header_file = File( From e8f8f5f99a91ed4a76726292c4d4492e7d27ad2c Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 3 Apr 2014 15:23:49 +0200 Subject: [PATCH 02/15] Added masks support in ErrorMap interface --- nipype/algorithms/eval.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nipype/algorithms/eval.py b/nipype/algorithms/eval.py index 89ef148baf..a3681f0be1 100644 --- a/nipype/algorithms/eval.py +++ b/nipype/algorithms/eval.py @@ -406,6 +406,7 @@ class ErrorMapInputSpec( BaseInterfaceInputSpec ): desc="Reference image. Requires the same dimensions as in_tst.") in_tst = File(exists=True, mandatory=True, desc="Test image. Requires the same dimensions as in_ref.") + mask = File(exists=True, desc="calculate overlap only within this mask.") method = traits.Enum( "squared_diff", "eucl", desc='', usedefault=True ) @@ -439,6 +440,7 @@ def _run_interface( self, runtime ): assert( ref_data.ndim == tst_data.ndim ) + if ( ref_data.ndim == 4 ): comps = ref_data.shape[-1] mapshape = ref_data.shape[:-1] @@ -449,6 +451,16 @@ def _run_interface( self, runtime ): refvector = ref_data.reshape(-1) tstvector = tst_data.reshape(-1) + if isdefined( self.inputs.mask ): + msk = nb.load( self.inputs.mask ).get_data() + + if ( mapshape != msk.shape ): + raise RuntimeError( "Mask should match volume shape") + + mskvector = msk.reshape(-1) + refvector = refvector * mskvector[:,np.newaxis] + tstvector = tstvector * mskvector[:,np.newaxis] + diffvector = (tstvector-refvector)**2 if ( ref_data.ndim > 1 ): diffvector = np.sum( diffvector, axis=1 ) From 140f4b68966809bb5ae6f3e13f91c07e2e6d65f7 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 3 Apr 2014 15:43:33 +0200 Subject: [PATCH 03/15] more informative message --- nipype/algorithms/eval.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nipype/algorithms/eval.py b/nipype/algorithms/eval.py index a3681f0be1..50b5a53794 100644 --- a/nipype/algorithms/eval.py +++ b/nipype/algorithms/eval.py @@ -455,7 +455,9 @@ def _run_interface( self, runtime ): msk = nb.load( self.inputs.mask ).get_data() if ( mapshape != msk.shape ): - raise RuntimeError( "Mask should match volume shape") + raise RuntimeError( "Mask should match volume shape, \ + mask is %s and volume is %s" % + ( list(msk.shape), list(mapshape) ) ) mskvector = msk.reshape(-1) refvector = refvector * mskvector[:,np.newaxis] From 4387136136dbc89cc2af58bb2f4d372f5f711ff3 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 3 Apr 2014 15:48:44 +0200 Subject: [PATCH 04/15] added squeeze of 1 comp dimensions --- nipype/algorithms/eval.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nipype/algorithms/eval.py b/nipype/algorithms/eval.py index 50b5a53794..18d979f570 100644 --- a/nipype/algorithms/eval.py +++ b/nipype/algorithms/eval.py @@ -435,8 +435,8 @@ class ErrorMap(BaseInterface): def _run_interface( self, runtime ): nii_ref = nb.load( self.inputs.in_ref ) - ref_data = nii_ref.get_data() - tst_data = nb.load( self.inputs.in_tst ).get_data() + ref_data = np.squeeze( nii_ref.get_data() ) + tst_data = np.squeeze( nb.load( self.inputs.in_tst ).get_data() ) assert( ref_data.ndim == tst_data.ndim ) @@ -456,7 +456,7 @@ def _run_interface( self, runtime ): if ( mapshape != msk.shape ): raise RuntimeError( "Mask should match volume shape, \ - mask is %s and volume is %s" % + mask is %s and volumes are %s" % ( list(msk.shape), list(mapshape) ) ) mskvector = msk.reshape(-1) From 07151c4e35343eb19c7f508fd6e1f318c2f84f14 Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 4 Apr 2014 17:40:04 +0200 Subject: [PATCH 05/15] Fixed error forming an extension of a filename --- nipype/algorithms/eval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nipype/algorithms/eval.py b/nipype/algorithms/eval.py index 18d979f570..f845bec2c4 100644 --- a/nipype/algorithms/eval.py +++ b/nipype/algorithms/eval.py @@ -481,7 +481,7 @@ def _run_interface( self, runtime ): fname,ext = op.splitext( op.basename( self.inputs.in_tst ) ) if ext=='.gz': fname,ext2 = op.splitext( fname ) - ext = ext + ext2 + ext = ext2 + ext self._out_file = op.abspath( fname + "_errmap" + ext ) else: self._out_file = self.inputs.out_map From 87d95cbe59bb7cc5ca850d07ba4299b2de27b98d Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 22 May 2014 15:54:22 +0200 Subject: [PATCH 06/15] Fixing errors to attend #830 --- nipype/algorithms/tests/test_auto_Distance.py | 2 +- nipype/algorithms/tests/test_auto_ErrorMap.py | 32 +++++++++++++++++++ .../tests/test_auto_FuzzyOverlap.py | 2 +- nipype/algorithms/tests/test_auto_Overlap.py | 2 +- 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 nipype/algorithms/tests/test_auto_ErrorMap.py diff --git a/nipype/algorithms/tests/test_auto_Distance.py b/nipype/algorithms/tests/test_auto_Distance.py index dd6f682de2..8738003eeb 100644 --- a/nipype/algorithms/tests/test_auto_Distance.py +++ b/nipype/algorithms/tests/test_auto_Distance.py @@ -1,6 +1,6 @@ # AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT from nipype.testing import assert_equal -from nipype.algorithms.misc import Distance +from nipype.algorithms.eval import Distance def test_Distance_inputs(): input_map = dict(ignore_exception=dict(nohash=True, diff --git a/nipype/algorithms/tests/test_auto_ErrorMap.py b/nipype/algorithms/tests/test_auto_ErrorMap.py new file mode 100644 index 0000000000..a71dd69a63 --- /dev/null +++ b/nipype/algorithms/tests/test_auto_ErrorMap.py @@ -0,0 +1,32 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from nipype.testing import assert_equal +from nipype.algorithms.eval import ErrorMap + +def test_ErrorMap_inputs(): + input_map = dict(ignore_exception=dict(nohash=True, + usedefault=True, + ), + in_ref=dict(mandatory=True, + ), + in_tst=dict(mandatory=True, + ), + mask=dict(), + method=dict(usedefault=True, + ), + out_map=dict(), + ) + inputs = ErrorMap.input_spec() + + for key, metadata in input_map.items(): + for metakey, value in metadata.items(): + yield assert_equal, getattr(inputs.traits()[key], metakey), value + +def test_ErrorMap_outputs(): + output_map = dict(out_map=dict(), + ) + outputs = ErrorMap.output_spec() + + for key, metadata in output_map.items(): + for metakey, value in metadata.items(): + yield assert_equal, getattr(outputs.traits()[key], metakey), value + diff --git a/nipype/algorithms/tests/test_auto_FuzzyOverlap.py b/nipype/algorithms/tests/test_auto_FuzzyOverlap.py index f9be02dc31..fc3b155eb6 100644 --- a/nipype/algorithms/tests/test_auto_FuzzyOverlap.py +++ b/nipype/algorithms/tests/test_auto_FuzzyOverlap.py @@ -1,6 +1,6 @@ # AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT from nipype.testing import assert_equal -from nipype.algorithms.misc import FuzzyOverlap +from nipype.algorithms.eval import FuzzyOverlap def test_FuzzyOverlap_inputs(): input_map = dict(ignore_exception=dict(nohash=True, diff --git a/nipype/algorithms/tests/test_auto_Overlap.py b/nipype/algorithms/tests/test_auto_Overlap.py index 331390e9b5..e9bb205156 100644 --- a/nipype/algorithms/tests/test_auto_Overlap.py +++ b/nipype/algorithms/tests/test_auto_Overlap.py @@ -1,6 +1,6 @@ # AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT from nipype.testing import assert_equal -from nipype.algorithms.misc import Overlap +from nipype.algorithms.eval import Overlap def test_Overlap_inputs(): input_map = dict(ignore_exception=dict(nohash=True, From dce38ccd22b53473431da233378fb258c622576c Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Thu, 22 May 2014 17:47:52 +0200 Subject: [PATCH 07/15] Minor changes to satisfy #830 --- CHANGES | 3 +++ nipype/algorithms/misc.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index 7ad890a151..e5a97b3029 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,9 @@ Next Release ============ * ENH: New ANTs interface: ApplyTransformsToPoints +* ENH: New evaluation algorithms. Now Distance, Overlap, and FuzzyOverlap + are found in nipype.algorithms.eval instead of misc +* ENH: New evaluation algorithm: ErrorMap (a voxel-wise diff map). * FIX: MRTrix tracking algorithms were ignoring mask parameters. Release 0.9.2 (January 31, 2014) diff --git a/nipype/algorithms/misc.py b/nipype/algorithms/misc.py index 3c144f77f1..27336b8261 100644 --- a/nipype/algorithms/misc.py +++ b/nipype/algorithms/misc.py @@ -28,6 +28,10 @@ from .. import logging +from .. import warnings +warnings.warn('Evaluation interfaces (Distance, Overlap, FuzzyOverlap) + have been moved to nipype.algorithms.eval' ) + from ..interfaces.base import (BaseInterface, traits, TraitedSpec, File, InputMultiPath, OutputMultiPath, BaseInterfaceInputSpec, isdefined) From 3ac78d80488a8d5de82fc434aa604a15d2f7b4e0 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Tue, 27 May 2014 16:59:07 +0200 Subject: [PATCH 08/15] Backwards compatibility in #830 --- nipype/algorithms/{eval.py => metrics.py} | 0 nipype/algorithms/misc.py | 22 ++++++++++++++++--- nipype/algorithms/tests/test_auto_Distance.py | 2 +- nipype/algorithms/tests/test_auto_ErrorMap.py | 2 +- .../tests/test_auto_FuzzyOverlap.py | 2 +- nipype/algorithms/tests/test_auto_Overlap.py | 2 +- 6 files changed, 23 insertions(+), 7 deletions(-) rename nipype/algorithms/{eval.py => metrics.py} (100%) diff --git a/nipype/algorithms/eval.py b/nipype/algorithms/metrics.py similarity index 100% rename from nipype/algorithms/eval.py rename to nipype/algorithms/metrics.py diff --git a/nipype/algorithms/misc.py b/nipype/algorithms/misc.py index 27336b8261..fc102c5828 100644 --- a/nipype/algorithms/misc.py +++ b/nipype/algorithms/misc.py @@ -28,10 +28,9 @@ from .. import logging -from .. import warnings -warnings.warn('Evaluation interfaces (Distance, Overlap, FuzzyOverlap) - have been moved to nipype.algorithms.eval' ) +import warnings +import metrics as nam from ..interfaces.base import (BaseInterface, traits, TraitedSpec, File, InputMultiPath, OutputMultiPath, BaseInterfaceInputSpec, isdefined) @@ -842,3 +841,20 @@ def calc_moments(timeseries_file, moment): m3 = stats.moment(timeseries, moment, axis=0) zero = (m2 == 0) return np.where(zero, 0, m3 / m2**(moment/2.0)) + +# Deprecated interfaces --------------------------------------------------------- +class Distance( nam.Distance ): + def __init__(self, **inputs): + super(nam.Distance, self).__init__(**inputs) + warnings.warn("This interface has been moved from misc to metrics", DeprecationWarning) + +class Overlap( nam.Overlap ): + def __init__(self, **inputs): + super(nam.Overlap, self).__init__(**inputs) + warnings.warn("This interface has been moved from misc to metrics", DeprecationWarning) + + +class FuzzyOverlap( nam.FuzzyOverlap ): + def __init__(self, **inputs): + super(nam.FuzzyOverlap, self).__init__(**inputs) + warnings.warn("This interface has been moved from misc to metrics", DeprecationWarning) diff --git a/nipype/algorithms/tests/test_auto_Distance.py b/nipype/algorithms/tests/test_auto_Distance.py index 8738003eeb..dd6f682de2 100644 --- a/nipype/algorithms/tests/test_auto_Distance.py +++ b/nipype/algorithms/tests/test_auto_Distance.py @@ -1,6 +1,6 @@ # AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT from nipype.testing import assert_equal -from nipype.algorithms.eval import Distance +from nipype.algorithms.misc import Distance def test_Distance_inputs(): input_map = dict(ignore_exception=dict(nohash=True, diff --git a/nipype/algorithms/tests/test_auto_ErrorMap.py b/nipype/algorithms/tests/test_auto_ErrorMap.py index a71dd69a63..324c2ff229 100644 --- a/nipype/algorithms/tests/test_auto_ErrorMap.py +++ b/nipype/algorithms/tests/test_auto_ErrorMap.py @@ -1,6 +1,6 @@ # AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT from nipype.testing import assert_equal -from nipype.algorithms.eval import ErrorMap +from nipype.algorithms.metrics import ErrorMap def test_ErrorMap_inputs(): input_map = dict(ignore_exception=dict(nohash=True, diff --git a/nipype/algorithms/tests/test_auto_FuzzyOverlap.py b/nipype/algorithms/tests/test_auto_FuzzyOverlap.py index fc3b155eb6..f9be02dc31 100644 --- a/nipype/algorithms/tests/test_auto_FuzzyOverlap.py +++ b/nipype/algorithms/tests/test_auto_FuzzyOverlap.py @@ -1,6 +1,6 @@ # AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT from nipype.testing import assert_equal -from nipype.algorithms.eval import FuzzyOverlap +from nipype.algorithms.misc import FuzzyOverlap def test_FuzzyOverlap_inputs(): input_map = dict(ignore_exception=dict(nohash=True, diff --git a/nipype/algorithms/tests/test_auto_Overlap.py b/nipype/algorithms/tests/test_auto_Overlap.py index e9bb205156..331390e9b5 100644 --- a/nipype/algorithms/tests/test_auto_Overlap.py +++ b/nipype/algorithms/tests/test_auto_Overlap.py @@ -1,6 +1,6 @@ # AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT from nipype.testing import assert_equal -from nipype.algorithms.eval import Overlap +from nipype.algorithms.misc import Overlap def test_Overlap_inputs(): input_map = dict(ignore_exception=dict(nohash=True, From dc24afc9db1c4a157e47bad47617218896b9393b Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Tue, 27 May 2014 17:04:47 +0200 Subject: [PATCH 09/15] Updated CHANGES --- CHANGES | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index e5a97b3029..aabec6fc02 100644 --- a/CHANGES +++ b/CHANGES @@ -2,9 +2,9 @@ Next Release ============ * ENH: New ANTs interface: ApplyTransformsToPoints -* ENH: New evaluation algorithms. Now Distance, Overlap, and FuzzyOverlap - are found in nipype.algorithms.eval instead of misc -* ENH: New evaluation algorithm: ErrorMap (a voxel-wise diff map). +* ENH: New metrics group in algorithms. Now Distance, Overlap, and FuzzyOverlap + are found in nipype.algorithms.metrics instead of misc +* ENH: New interface in algorithms.metrics: ErrorMap (a voxel-wise diff map). * FIX: MRTrix tracking algorithms were ignoring mask parameters. Release 0.9.2 (January 31, 2014) From cf0af2433454828a4be4fe8ecd38eedfdd880fb5 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Wed, 28 May 2014 10:46:56 +0200 Subject: [PATCH 10/15] Moved the new Similarity in 4D to metrics Close #802. Please merge #830 before this PR --- nipype/algorithms/metrics.py | 92 +++++++++++++++++++++++++++++++++ nipype/interfaces/nipy/utils.py | 5 ++ 2 files changed, 97 insertions(+) diff --git a/nipype/algorithms/metrics.py b/nipype/algorithms/metrics.py index f845bec2c4..4fb739171a 100644 --- a/nipype/algorithms/metrics.py +++ b/nipype/algorithms/metrics.py @@ -495,3 +495,95 @@ def _list_outputs(self): outputs['out_map'] = self._out_file return outputs + + +class SimilarityInputSpec(BaseInterfaceInputSpec): + + volume1 = File(exists=True, desc="3D/4D volume", mandatory=True) + volume2 = File(exists=True, desc="3D/4D volume", mandatory=True) + mask1 = File(exists=True, desc="3D volume") + mask2 = File(exists=True, desc="3D volume") + metric = traits.Either(traits.Enum('cc', 'cr', 'crl1', 'mi', 'nmi', 'slr'), + traits.Callable(), + desc="""str or callable +Cost-function for assessing image similarity. If a string, +one of 'cc': correlation coefficient, 'cr': correlation +ratio, 'crl1': L1-norm based correlation ratio, 'mi': mutual +information, 'nmi': normalized mutual information, 'slr': +supervised log-likelihood ratio. If a callable, it should +take a two-dimensional array representing the image joint +histogram as an input and return a float.""", usedefault=True) + + +class SimilarityOutputSpec(TraitedSpec): + similarity = traits.List( traits.Float(desc="Similarity between volume 1 and 2, frame by frame")) + + +class Similarity(BaseInterface): + """Calculates similarity between two 3D or 4D volumes. Both volumes have to be in + the same coordinate system, same space within that coordinate system and + with the same voxel dimensions. + + .. note:: + + This interface is an extension of nipype.interfaces.nipy.utils.Similarity + to support 4D files. + + Example + ------- + >>> from nipype.interfaces.nipy.utils import Similarity + >>> similarity = Similarity() + >>> similarity.inputs.volume1 = 'rc1s1.nii' + >>> similarity.inputs.volume2 = 'rc1s2.nii' + >>> similarity.inputs.mask1 = 'mask.nii' + >>> similarity.inputs.mask2 = 'mask.nii' + >>> similarity.inputs.metric = 'cr' + >>> res = similarity.run() # doctest: +SKIP + """ + + input_spec = SimilarityInputSpec + output_spec = SimilarityOutputSpec + + def _run_interface(self, runtime): + + vol1_nii = nb.load(self.inputs.volume1) + vol2_nii = nb.load(self.inputs.volume2) + + dims = vol1_nii.get_data().ndim + + if dims==3 or dims==2: + vols1 = [ vol1_nii ] + vols2 = [ vol2_nii ] + if dims==4: + vols1 = nb.four_to_three( vol1_nii ) + vols2 = nb.four_to_three( vol2_nii ) + + if dims<2 or dims>4: + raise RuntimeError( 'Image dimensions not supported (detected %dD file)' % dims ) + + if isdefined(self.inputs.mask1): + mask1 = nb.load(self.inputs.mask1).get_data() == 1 + else: + mask1 = None + + if isdefined(self.inputs.mask2): + mask2 = nb.load(self.inputs.mask2).get_data() == 1 + else: + mask2 = None + + self._similarity = [] + + for ts1,ts2 in zip( vols1, vols2 ): + histreg = HistogramRegistration(from_img = ts1, + to_img = ts2, + similarity=self.inputs.metric, + from_mask = mask1, + to_mask = mask2) + self._similarity.append( histreg.eval(Affine()) ) + + return runtime + + def _list_outputs(self): + outputs = self._outputs().get() + outputs['similarity'] = self._similarity + return outputs diff --git a/nipype/interfaces/nipy/utils.py b/nipype/interfaces/nipy/utils.py index f9a989fa8f..1950459695 100644 --- a/nipype/interfaces/nipy/utils.py +++ b/nipype/interfaces/nipy/utils.py @@ -68,6 +68,11 @@ class Similarity(BaseInterface): input_spec = SimilarityInputSpec output_spec = SimilarityOutputSpec + def __init__(self, **inputs): + warnings.warn("This interface is deprecated. Please use nipy.algorithms.metrics.Similarity", + DeprecationWarning) + super(BaseInterface,self).__init__(**inputs) + def _run_interface(self, runtime): vol1_nii = nb.load(self.inputs.volume1) From f94e4dc33318062b690dbb9d55191e5e8dea7213 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Wed, 28 May 2014 11:23:35 +0200 Subject: [PATCH 11/15] updated changelog --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index aabec6fc02..3afd762e83 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,8 @@ Next Release ============ +* ENH: nipy.utils.Similarity has been deprecated by algorithms.metrics.Similarity + which is an extension of the former to allow 4D images. * ENH: New ANTs interface: ApplyTransformsToPoints * ENH: New metrics group in algorithms. Now Distance, Overlap, and FuzzyOverlap are found in nipype.algorithms.metrics instead of misc From 98eb832eedcee11fcfc7b321d0e25b94cc19ec23 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 2 Jun 2014 15:36:18 +0200 Subject: [PATCH 12/15] Added version 0.10.0 to all DeprecationWarnings --- nipype/algorithms/misc.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nipype/algorithms/misc.py b/nipype/algorithms/misc.py index fc102c5828..59e3dfe561 100644 --- a/nipype/algorithms/misc.py +++ b/nipype/algorithms/misc.py @@ -846,15 +846,21 @@ def calc_moments(timeseries_file, moment): class Distance( nam.Distance ): def __init__(self, **inputs): super(nam.Distance, self).__init__(**inputs) - warnings.warn("This interface has been moved from misc to metrics", DeprecationWarning) + warnings.warn(("This interface has been deprecated since 0.10.0," + " please use nipype.algorithms.metrics.Distance"), + DeprecationWarning) class Overlap( nam.Overlap ): def __init__(self, **inputs): super(nam.Overlap, self).__init__(**inputs) - warnings.warn("This interface has been moved from misc to metrics", DeprecationWarning) + warnings.warn(("This interface has been deprecated since 0.10.0," + " please use nipype.algorithms.metrics.Overlap"), + DeprecationWarning) class FuzzyOverlap( nam.FuzzyOverlap ): def __init__(self, **inputs): super(nam.FuzzyOverlap, self).__init__(**inputs) - warnings.warn("This interface has been moved from misc to metrics", DeprecationWarning) + warnings.warn(("This interface has been deprecated since 0.10.0," + " please use nipype.algorithms.metrics.FuzzyOverlap"), + DeprecationWarning) From 7c8846f9908f43a73b87af0a913ef17e29390022 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 2 Jun 2014 16:51:52 +0200 Subject: [PATCH 13/15] Fixed test in new Similarity interface Also fixed constructor of old interface to throw the DeprecationWarning --- nipype/algorithms/metrics.py | 2 +- nipype/interfaces/nipy/utils.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/nipype/algorithms/metrics.py b/nipype/algorithms/metrics.py index 4bbeef19b6..2f78deddf7 100644 --- a/nipype/algorithms/metrics.py +++ b/nipype/algorithms/metrics.py @@ -529,7 +529,7 @@ class Similarity(BaseInterface): Example ------- - >>> from nipype.interfaces.nipy.utils import Similarity + >>> from nipype.algorithms.metrics import Similarity >>> similarity = Similarity() >>> similarity.inputs.volume1 = 'rc1s1.nii' >>> similarity.inputs.volume2 = 'rc1s2.nii' diff --git a/nipype/interfaces/nipy/utils.py b/nipype/interfaces/nipy/utils.py index 529811a8df..c929f48f77 100644 --- a/nipype/interfaces/nipy/utils.py +++ b/nipype/interfaces/nipy/utils.py @@ -26,7 +26,6 @@ class SimilarityInputSpec(BaseInterfaceInputSpec): - volume1 = File(exists=True, desc="3D volume", mandatory=True) volume2 = File(exists=True, desc="3D volume", mandatory=True) mask1 = File(exists=True, desc="3D volume") @@ -44,7 +43,6 @@ class SimilarityInputSpec(BaseInterfaceInputSpec): class SimilarityOutputSpec(TraitedSpec): - similarity = traits.Float(desc="Similarity between volume 1 and 2") @@ -72,7 +70,7 @@ def __init__(self, **inputs): warnings.warn(("This interface is deprecated since 0.10.0." " Please use nipype.algorithms.metrics.Similarity"), DeprecationWarning) - super(BaseInterface,self).__init__(**inputs) + super(Similarity,self).__init__(**inputs) def _run_interface(self, runtime): From 960a3744152e2910441db67e5add449c3bb8dc5f Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 2 Jun 2014 17:48:20 +0200 Subject: [PATCH 14/15] Added package_check for nipy in new Similarity4D Additionally, docstrings have been improved for several methods, especially indicating deprecated classes. --- nipype/algorithms/metrics.py | 30 ++++++++++++++------ nipype/algorithms/misc.py | 49 +++++++++++++++++++++------------ nipype/interfaces/nipy/utils.py | 3 ++ 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/nipype/algorithms/metrics.py b/nipype/algorithms/metrics.py index 2f78deddf7..471d6a76cf 100644 --- a/nipype/algorithms/metrics.py +++ b/nipype/algorithms/metrics.py @@ -28,6 +28,7 @@ import scipy.stats as stats from .. import logging +from ..utils.misc import package_check from ..interfaces.base import (BaseInterface, traits, TraitedSpec, File, InputMultiPath, OutputMultiPath, @@ -69,9 +70,8 @@ class DistanceOutputSpec(TraitedSpec): class Distance(BaseInterface): - ''' - Calculates distance between two volumes. - ''' + """Calculates distance between two volumes. + """ input_spec = DistanceInputSpec output_spec = DistanceOutputSpec @@ -232,8 +232,7 @@ class OverlapOutputSpec(TraitedSpec): class Overlap(BaseInterface): - """ - Calculates various overlap measures between two maps. + """Calculates various overlap measures between two maps. Example ------- @@ -313,7 +312,7 @@ class FuzzyOverlapOutputSpec(TraitedSpec): class FuzzyOverlap(BaseInterface): - """ Calculates various overlap measures between two maps, using the fuzzy + """Calculates various overlap measures between two maps, using the fuzzy definition proposed in: Crum et al., Generalized Overlap Measures for Evaluation and Validation in Medical Image Analysis, IEEE Trans. Med. Ima. 25(11),pp 1451-1461, Nov. 2006. @@ -524,8 +523,9 @@ class Similarity(BaseInterface): the same coordinate system, same space within that coordinate system and with the same voxel dimensions. - .. note:: This interface is an extension of nipype.interfaces.nipy.utils.Similarity - to support 4D files. + .. note:: This interface is an extension of + :py:class:`nipype.interfaces.nipy.utils.Similarity` to support 4D files. + Requires :py:mod:`nipy` Example ------- @@ -541,8 +541,22 @@ class Similarity(BaseInterface): input_spec = SimilarityInputSpec output_spec = SimilarityOutputSpec + _have_nipy = True + + def __init__(self, **inputs): + try: + package_check('nipy') + except Exception, e: + self._have_nipy = False + super(Similarity,self).__init__(**inputs) + def _run_interface(self, runtime): + if not self._have_nipy: + raise RuntimeError('nipy is not installed') + + from nipy.algorithms.registration.histogram_registration import HistogramRegistration + from nipy.algorithms.registration.affine import Affine vol1_nii = nb.load(self.inputs.volume1) vol2_nii = nb.load(self.inputs.volume2) diff --git a/nipype/algorithms/misc.py b/nipype/algorithms/misc.py index 59e3dfe561..21cb12e2c5 100644 --- a/nipype/algorithms/misc.py +++ b/nipype/algorithms/misc.py @@ -64,10 +64,10 @@ class PickAtlasOutputSpec(TraitedSpec): class PickAtlas(BaseInterface): - ''' - Returns ROI masks given an atlas and a list of labels. Supports dilation + """Returns ROI masks given an atlas and a list of labels. Supports dilation and left right masking (assuming the atlas is properly aligned). - ''' + """ + input_spec = PickAtlasInputSpec output_spec = PickAtlasOutputSpec @@ -132,6 +132,8 @@ class SimpleThresholdOutputSpec(TraitedSpec): class SimpleThreshold(BaseInterface): + """Applies a threshold to input volumes + """ input_spec = SimpleThresholdInputSpec output_spec = SimpleThresholdOutputSpec @@ -182,10 +184,9 @@ class ModifyAffineOutputSpec(TraitedSpec): class ModifyAffine(BaseInterface): - ''' - Left multiplies the affine matrix with a specified values. Saves the volume + """Left multiplies the affine matrix with a specified values. Saves the volume as a nifti file. - ''' + """ input_spec = ModifyAffineInputSpec output_spec = ModifyAffineOutputSpec @@ -225,6 +226,8 @@ class CreateNiftiOutputSpec(TraitedSpec): class CreateNifti(BaseInterface): + """Creates a nifti volume + """ input_spec = CreateNiftiInputSpec output_spec = CreateNiftiOutputSpec @@ -344,8 +347,7 @@ class GunzipOutputSpec(TraitedSpec): class Gunzip(BaseInterface): - """ - + """Gunzip wrapper """ input_spec = GunzipInputSpec output_spec = GunzipOutputSpec @@ -409,8 +411,7 @@ class Matlab2CSVOutputSpec(TraitedSpec): class Matlab2CSV(BaseInterface): - """ - Simple interface to save the components of a MATLAB .mat file as a text + """Simple interface to save the components of a MATLAB .mat file as a text file with comma-separated values (CSVs). CSV files are easily loaded in R, for use in statistical processing. @@ -602,8 +603,7 @@ class MergeCSVFilesOutputSpec(TraitedSpec): class MergeCSVFiles(BaseInterface): - """ - This interface is designed to facilitate data loading in the R environment. + """This interface is designed to facilitate data loading in the R environment. It takes input CSV files and merges them into a single CSV file. If provided, it will also incorporate column heading names into the resulting CSV file. @@ -738,8 +738,7 @@ class AddCSVColumnOutputSpec(TraitedSpec): class AddCSVColumn(BaseInterface): - """ - Short interface to add an extra column and field to a text file + """Short interface to add an extra column and field to a text file Example ------- @@ -799,8 +798,7 @@ class CalculateNormalizedMomentsOutputSpec(TraitedSpec): class CalculateNormalizedMoments(BaseInterface): - """ - Calculates moments of timeseries. + """Calculates moments of timeseries. Example ------- @@ -827,8 +825,7 @@ def _list_outputs(self): def calc_moments(timeseries_file, moment): - """ - Returns nth moment (3 for skewness, 4 for kurtosis) of timeseries + """Returns nth moment (3 for skewness, 4 for kurtosis) of timeseries (list of values; one per timeseries). Keyword arguments: @@ -844,6 +841,11 @@ def calc_moments(timeseries_file, moment): # Deprecated interfaces --------------------------------------------------------- class Distance( nam.Distance ): + """Calculates distance between two volumes. + + .. deprecated:: 0.10.0 + Use :py:class:`nipype.algorithms.metrics.Distance` instead. + """ def __init__(self, **inputs): super(nam.Distance, self).__init__(**inputs) warnings.warn(("This interface has been deprecated since 0.10.0," @@ -851,6 +853,11 @@ def __init__(self, **inputs): DeprecationWarning) class Overlap( nam.Overlap ): + """Calculates various overlap measures between two maps. + + .. deprecated:: 0.10.0 + Use :py:class:`nipype.algorithms.metrics.Overlap` instead. + """ def __init__(self, **inputs): super(nam.Overlap, self).__init__(**inputs) warnings.warn(("This interface has been deprecated since 0.10.0," @@ -859,6 +866,12 @@ def __init__(self, **inputs): class FuzzyOverlap( nam.FuzzyOverlap ): + """Calculates various overlap measures between two maps, using a fuzzy + definition. + + .. deprecated:: 0.10.0 + Use :py:class:`nipype.algorithms.metrics.FuzzyOverlap` instead. + """ def __init__(self, **inputs): super(nam.FuzzyOverlap, self).__init__(**inputs) warnings.warn(("This interface has been deprecated since 0.10.0," diff --git a/nipype/interfaces/nipy/utils.py b/nipype/interfaces/nipy/utils.py index c929f48f77..8a6af7de96 100644 --- a/nipype/interfaces/nipy/utils.py +++ b/nipype/interfaces/nipy/utils.py @@ -51,6 +51,9 @@ class Similarity(BaseInterface): the same coordinate system, same space within that coordinate system and with the same voxel dimensions. + .. deprecated:: 0.10.0 + Use :py:class:`nipype.algorithms.metrics.Similarity` instead. + Example ------- >>> from nipype.interfaces.nipy.utils import Similarity From 664b93c0716340419c16b6dac067afdaeef7e13e Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 11 Jul 2014 18:58:33 +0200 Subject: [PATCH 15/15] Improvements on ErrorMap implementation Now it uses numpy.linalg.norm --- nipype/algorithms/metrics.py | 102 +++++++++--------- nipype/algorithms/tests/test_auto_ErrorMap.py | 3 +- .../algorithms/tests/test_auto_Similarity.py | 32 ++++++ 3 files changed, 87 insertions(+), 50 deletions(-) create mode 100644 nipype/algorithms/tests/test_auto_Similarity.py diff --git a/nipype/algorithms/metrics.py b/nipype/algorithms/metrics.py index 471d6a76cf..732a28e1b3 100644 --- a/nipype/algorithms/metrics.py +++ b/nipype/algorithms/metrics.py @@ -384,7 +384,7 @@ def _run_interface(self, runtime): diff+= w* ch nb.save(nb.Nifti1Image(diff, nb.load( self.inputs.in_ref[0]).get_affine(), - nb.load( self.inputs.in_ref[0]).get_header()), self.inputs.out_file ) + nb.load(self.inputs.in_ref[0]).get_header()), self.inputs.out_file) return runtime @@ -400,20 +400,20 @@ def _list_outputs(self): return outputs -class ErrorMapInputSpec( BaseInterfaceInputSpec ): +class ErrorMapInputSpec(BaseInterfaceInputSpec): in_ref = File(exists=True, mandatory=True, desc="Reference image. Requires the same dimensions as in_tst.") in_tst = File(exists=True, mandatory=True, desc="Test image. Requires the same dimensions as in_ref.") mask = File(exists=True, desc="calculate overlap only within this mask.") - method = traits.Enum( "squared_diff", "eucl", - desc='', - usedefault=True ) - out_map = File( desc="Name for the output file" ) + metric = traits.Enum("sqeuclidean", "euclidean", + desc='error map metric (as implemented in scipy cdist)', + usedefault=True, mandatory=True) + out_map = File(desc="Name for the output file") -class ErrorMapOutputSpec(TraitedSpec): - out_map = File(exists=True, desc="resulting error map" ) +class ErrorMapOutputSpec(TraitedSpec): + out_map = File(exists=True, desc="resulting error map") class ErrorMap(BaseInterface): @@ -429,75 +429,79 @@ class ErrorMap(BaseInterface): """ input_spec = ErrorMapInputSpec output_spec = ErrorMapOutputSpec - _out_file = "" - - - def _run_interface( self, runtime ): - nii_ref = nb.load( self.inputs.in_ref ) - ref_data = np.squeeze( nii_ref.get_data() ) - tst_data = np.squeeze( nb.load( self.inputs.in_tst ).get_data() ) + _out_file = '' - assert( ref_data.ndim == tst_data.ndim ) + def _run_interface(self, runtime): + from scipy.spatial.distance import cdist, pdist + nii_ref = nb.load(self.inputs.in_ref) + ref_data = np.squeeze(nii_ref.get_data()) + tst_data = np.squeeze(nb.load(self.inputs.in_tst).get_data()) + assert(ref_data.ndim == tst_data.ndim) + comps = 1 + mapshape = ref_data.shape - if ( ref_data.ndim == 4 ): + if (ref_data.ndim == 4): comps = ref_data.shape[-1] mapshape = ref_data.shape[:-1] - refvector = np.reshape( ref_data, (-1,comps)) - tstvector = np.reshape( tst_data, (-1,comps)) - else: - mapshape = ref_data.shape - refvector = ref_data.reshape(-1) - tstvector = tst_data.reshape(-1) - if isdefined( self.inputs.mask ): + if isdefined(self.inputs.mask): msk = nb.load( self.inputs.mask ).get_data() + if (mapshape != msk.shape): + raise RuntimeError("Mask should match volume shape, \ + mask is %s and volumes are %s" % + (list(msk.shape), list(mapshape))) + else: + msk = np.ones(shape=mapshape) + + mskvector = msk.reshape(-1) + msk_idxs = np.where(mskvector==1) + refvector = ref_data.reshape(-1,comps)[msk_idxs].astype(np.float32) + tstvector = tst_data.reshape(-1,comps)[msk_idxs].astype(np.float32) + diffvector = (refvector-tstvector) + + if self.inputs.metric == 'sqeuclidean': + errvector = diffvector**2 + elif self.inputs.metric == 'euclidean': + X = np.hstack((refvector, tstvector)) + errvector = np.linalg.norm(X, axis=1) + + if (comps > 1): + errvector = np.sum(errvector, axis=1) + else: + errvector = np.squeeze(errvector) - if ( mapshape != msk.shape ): - raise RuntimeError( "Mask should match volume shape, \ - mask is %s and volumes are %s" % - ( list(msk.shape), list(mapshape) ) ) - - mskvector = msk.reshape(-1) - refvector = refvector * mskvector[:,np.newaxis] - tstvector = tstvector * mskvector[:,np.newaxis] - - diffvector = (tstvector-refvector)**2 - if ( ref_data.ndim > 1 ): - diffvector = np.sum( diffvector, axis=1 ) + errvectorexp = np.zeros_like(mskvector) + errvectorexp[msk_idxs] = errvector - diffmap = diffvector.reshape( mapshape ) + errmap = errvectorexp.reshape(mapshape) hdr = nii_ref.get_header().copy() - hdr.set_data_dtype( np.float32 ) + hdr.set_data_dtype(np.float32) hdr['data_type'] = 16 - hdr.set_data_shape( diffmap.shape ) + hdr.set_data_shape(mapshape) - niimap = nb.Nifti1Image( diffmap.astype( np.float32 ), - nii_ref.get_affine(), hdr ) - - if not isdefined( self.inputs.out_map ): - fname,ext = op.splitext( op.basename( self.inputs.in_tst ) ) + if not isdefined(self.inputs.out_map): + fname,ext = op.splitext(op.basename(self.inputs.in_tst)) if ext=='.gz': - fname,ext2 = op.splitext( fname ) + fname,ext2 = op.splitext(fname) ext = ext2 + ext - self._out_file = op.abspath( fname + "_errmap" + ext ) + self._out_file = op.abspath(fname + "_errmap" + ext) else: self._out_file = self.inputs.out_map - nb.save( niimap, self._out_file ) + nb.Nifti1Image(errmap.astype(np.float32), nii_ref.get_affine(), + hdr).to_filename(self._out_file) return runtime def _list_outputs(self): outputs = self.output_spec().get() outputs['out_map'] = self._out_file - return outputs class SimilarityInputSpec(BaseInterfaceInputSpec): - volume1 = File(exists=True, desc="3D/4D volume", mandatory=True) volume2 = File(exists=True, desc="3D/4D volume", mandatory=True) mask1 = File(exists=True, desc="3D volume") diff --git a/nipype/algorithms/tests/test_auto_ErrorMap.py b/nipype/algorithms/tests/test_auto_ErrorMap.py index 324c2ff229..99048aea9c 100644 --- a/nipype/algorithms/tests/test_auto_ErrorMap.py +++ b/nipype/algorithms/tests/test_auto_ErrorMap.py @@ -11,7 +11,8 @@ def test_ErrorMap_inputs(): in_tst=dict(mandatory=True, ), mask=dict(), - method=dict(usedefault=True, + metric=dict(mandatory=True, + usedefault=True, ), out_map=dict(), ) diff --git a/nipype/algorithms/tests/test_auto_Similarity.py b/nipype/algorithms/tests/test_auto_Similarity.py new file mode 100644 index 0000000000..c5f87f33d8 --- /dev/null +++ b/nipype/algorithms/tests/test_auto_Similarity.py @@ -0,0 +1,32 @@ +# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT +from nipype.testing import assert_equal +from nipype.algorithms.metrics import Similarity + +def test_Similarity_inputs(): + input_map = dict(ignore_exception=dict(nohash=True, + usedefault=True, + ), + mask1=dict(), + mask2=dict(), + metric=dict(usedefault=True, + ), + volume1=dict(mandatory=True, + ), + volume2=dict(mandatory=True, + ), + ) + inputs = Similarity.input_spec() + + for key, metadata in input_map.items(): + for metakey, value in metadata.items(): + yield assert_equal, getattr(inputs.traits()[key], metakey), value + +def test_Similarity_outputs(): + output_map = dict(similarity=dict(), + ) + outputs = Similarity.output_spec() + + for key, metadata in output_map.items(): + for metakey, value in metadata.items(): + yield assert_equal, getattr(outputs.traits()[key], metakey), value +