diff --git a/nipype/interfaces/base.py b/nipype/interfaces/base.py index 5abb9aeabd..ec1ab8c43d 100644 --- a/nipype/interfaces/base.py +++ b/nipype/interfaces/base.py @@ -38,8 +38,8 @@ from ..utils.filemanip import (md5, hash_infile, FileNotFoundError, hash_timestamp, split_filename, to_str) from .traits_extension import ( - traits, Undefined, TraitDictObject, TraitListObject, TraitError, isdefined, File, - Directory, DictStrStr, has_metadata) + traits, Undefined, TraitDictObject, TraitListObject, TraitError, isdefined, + File, Directory, DictStrStr, has_metadata, ImageFile) from ..external.due import due runtime_profile = str2bool(config.get('execution', 'profile_runtime')) diff --git a/nipype/interfaces/fsl/tests/test_auto_Eddy.py b/nipype/interfaces/fsl/tests/test_auto_Eddy.py index e47633fb07..c5f521045f 100644 --- a/nipype/interfaces/fsl/tests/test_auto_Eddy.py +++ b/nipype/interfaces/fsl/tests/test_auto_Eddy.py @@ -88,9 +88,6 @@ def test_Eddy_inputs(): def test_Eddy_outputs(): output_map = dict(out_corrected=dict(), out_movement_rms=dict(), - out_outlier_map=dict(), - out_outlier_n_sd_map=dict(), - out_outlier_n_sqr_sd_map=dict(), out_outlier_report=dict(), out_parameter=dict(), out_restricted_movement_rms=dict(), diff --git a/nipype/interfaces/fsl/tests/test_auto_TOPUP.py b/nipype/interfaces/fsl/tests/test_auto_TOPUP.py index 01f50670ba..28083c6dc0 100644 --- a/nipype/interfaces/fsl/tests/test_auto_TOPUP.py +++ b/nipype/interfaces/fsl/tests/test_auto_TOPUP.py @@ -54,10 +54,6 @@ def test_TOPUP_inputs(): name_source=['in_file'], name_template='%s_field', ), - out_warp_prefix=dict(argstr='--dfout=%s', - hash_files=False, - usedefault=True, - ), out_jac_prefix=dict(argstr='--jacout=%s', hash_files=False, usedefault=True, @@ -68,6 +64,10 @@ def test_TOPUP_inputs(): name_source=['in_file'], name_template='%s_topup.log', ), + out_warp_prefix=dict(argstr='--dfout=%s', + hash_files=False, + usedefault=True, + ), output_type=dict(), readout_times=dict(mandatory=True, requires=['encoding_direction'], @@ -104,10 +104,10 @@ def test_TOPUP_outputs(): out_enc_file=dict(), out_field=dict(), out_fieldcoef=dict(), + out_jacs=dict(), out_logfile=dict(), out_movpar=dict(), out_warps=dict(), - out_jacs=dict(), ) outputs = TOPUP.output_spec() diff --git a/nipype/interfaces/spm/base.py b/nipype/interfaces/spm/base.py index 25ab67d412..6c3fbab32e 100644 --- a/nipype/interfaces/spm/base.py +++ b/nipype/interfaces/spm/base.py @@ -30,7 +30,7 @@ from ... import logging from ...utils import spm_docs as sd, NUMPY_MMAP from ..base import (BaseInterface, traits, isdefined, InputMultiPath, - BaseInterfaceInputSpec, Directory, Undefined) + BaseInterfaceInputSpec, Directory, Undefined, ImageFile) from ..matlab import MatlabCommand from ...external.due import due, Doi, BibTeX @@ -532,3 +532,26 @@ def _make_matlab_command(self, contents, postscript=None): if postscript is not None: mscript += postscript return mscript + +class ImageFileSPM(ImageFile): + """ + Defines an ImageFile trait specific to SPM interfaces. + """ + + def __init__(self, value='', filter=None, auto_set=False, entries=0, + exists=False, types=['nifti1', 'nifti2'], + allow_compressed=False, **metadata): + """ Trait handles neuroimaging files. + + Parameters + ---------- + types : list + Strings of file format types accepted + compressed : boolean + Indicates whether the file format can compressed + """ + self.types = types + self.allow_compressed = allow_compressed + super(ImageFileSPM, self).__init__(value, filter, auto_set, entries, + exists, types, allow_compressed, + **metadata) diff --git a/nipype/interfaces/spm/preprocess.py b/nipype/interfaces/spm/preprocess.py index 322824f38d..0eebf3c6b8 100644 --- a/nipype/interfaces/spm/preprocess.py +++ b/nipype/interfaces/spm/preprocess.py @@ -25,14 +25,17 @@ from ..base import (OutputMultiPath, TraitedSpec, isdefined, traits, InputMultiPath, File) from .base import (SPMCommand, scans_for_fname, func_is_3d, - scans_for_fnames, SPMCommandInputSpec) + scans_for_fnames, SPMCommandInputSpec, ImageFileSPM) __docformat__ = 'restructuredtext' + class SliceTimingInputSpec(SPMCommandInputSpec): - in_files = InputMultiPath(traits.Either(traits.List(File(exists=True)), - File(exists=True)), field='scans', + in_files = InputMultiPath(traits.Either(traits.List(ImageFileSPM( + exists=True)), + ImageFileSPM(exists=True)), + field='scans', desc='list of filenames to apply slice timing', mandatory=True, copyfile=False) num_slices = traits.Int(field='nslices', @@ -116,8 +119,9 @@ def _list_outputs(self): class RealignInputSpec(SPMCommandInputSpec): - in_files = InputMultiPath(traits.Either(traits.List(File(exists=True)), - File(exists=True)), field='data', + in_files = InputMultiPath(traits.Either(traits.List( + ImageFileSPM(exists=True)), + ImageFileSPM(exists=True)), field='data', mandatory=True, copyfile=True, desc='list of filenames to realign') jobtype = traits.Enum('estwrite', 'estimate', 'write', @@ -270,11 +274,12 @@ def _list_outputs(self): class CoregisterInputSpec(SPMCommandInputSpec): - target = File(exists=True, field='ref', mandatory=True, - desc='reference file to register to', copyfile=False) - source = InputMultiPath(File(exists=True), field='source', - desc='file to register to target', copyfile=True, - mandatory=True) + target = ImageFileSPM(exists=True, mandatory=True, + field='ref', desc='reference file to register to', + copyfile=False) + source = InputMultiPath(ImageFileSPM(exists=True), + field='source', desc='file to register to target', + copyfile=True, mandatory=True) jobtype = traits.Enum('estwrite', 'estimate', 'write', desc='one of: estimate, write, estwrite', usedefault=True) @@ -392,9 +397,9 @@ class NormalizeInputSpec(SPMCommandInputSpec): desc='template file to normalize to', mandatory=True, xor=['parameter_file'], copyfile=False) - source = InputMultiPath(File(exists=True), field='subj.source', + source = InputMultiPath(ImageFileSPM(exists=True), + field='subj.source', xor=['parameter_file'], desc='file to normalize to template', - xor=['parameter_file'], mandatory=True, copyfile=True) jobtype = traits.Enum('estwrite', 'est', 'write', usedefault=True, desc='Estimate, Write or do both') @@ -555,22 +560,22 @@ def _list_outputs(self): class Normalize12InputSpec(SPMCommandInputSpec): - image_to_align = File(exists=True, field='subj.vol', + image_to_align = ImageFileSPM(exists=True, field='subj.vol', desc=('file to estimate normalization parameters ' 'with'), xor=['deformation_file'], mandatory=True, copyfile=True) apply_to_files = InputMultiPath( - traits.Either(File(exists=True), traits.List(File(exists=True))), + traits.Either(ImageFileSPM(exists=True), + traits.List(ImageFileSPM(exists=True))), field='subj.resample', desc='files to apply transformation to', copyfile=True) - deformation_file = File(field='subj.def', mandatory=True, - xor=['image_to_align', 'tpm'], + deformation_file = ImageFileSPM(field='subj.def', mandatory=True, + xor=['image_to_align', 'tpm'], copyfile=False, desc=('file y_*.nii containing 3 deformation ' 'fields for the deformation in x, y and z ' - 'dimension'), - copyfile=False) + 'dimension')) jobtype = traits.Enum('estwrite', 'est', 'write', usedefault=True, desc='Estimate, Write or do Both') bias_regularization = traits.Enum(0, 0.00001, 0.0001, 0.001, 0.01, 0.1, 1, @@ -721,7 +726,7 @@ def _list_outputs(self): class SegmentInputSpec(SPMCommandInputSpec): - data = InputMultiPath(File(exists=True), field='data', + data = InputMultiPath(ImageFileSPM(exists=True), field='data', desc='one scan per subject', copyfile=False, mandatory=True) gm_output_type = traits.List(traits.Bool(), minlen=3, maxlen=3, @@ -890,10 +895,9 @@ def _list_outputs(self): class NewSegmentInputSpec(SPMCommandInputSpec): - channel_files = InputMultiPath(File(exists=True), + channel_files = InputMultiPath(ImageFileSPM(exists=True), mandatory=True, desc="A list of files to be segmented", - field='channel', copyfile=False, - mandatory=True) + field='channel', copyfile=False) channel_info = traits.Tuple(traits.Float(), traits.Float(), traits.Tuple(traits.Bool, traits.Bool), desc="""A tuple with the following fields: @@ -902,7 +906,7 @@ class NewSegmentInputSpec(SPMCommandInputSpec): - which maps to save (Corrected, Field) - a tuple of two boolean values""", field='channel') tissues = traits.List( - traits.Tuple(traits.Tuple(File(exists=True), traits.Int()), + traits.Tuple(traits.Tuple(ImageFileSPM(exists=True),traits.Int()), traits.Int(), traits.Tuple(traits.Bool, traits.Bool), traits.Tuple(traits.Bool, traits.Bool)), desc="""A list of tuples (one per tissue) with the following fields: @@ -1093,8 +1097,8 @@ def _list_outputs(self): class SmoothInputSpec(SPMCommandInputSpec): - in_files = InputMultiPath(File(exists=True), field='data', - desc='list of files to smooth', + in_files = InputMultiPath(ImageFileSPM(exists=True), + field='data', desc='list of files to smooth', mandatory=True, copyfile=False) fwhm = traits.Either(traits.List(traits.Float(), minlen=3, maxlen=3), traits.Float(), field='fwhm', @@ -1156,7 +1160,7 @@ def _list_outputs(self): class DARTELInputSpec(SPMCommandInputSpec): - image_files = traits.List(traits.List(File(exists=True)), + image_files = traits.List(traits.List(ImageFileSPM(exists=True)), desc="A list of files to be segmented", field='warp.images', copyfile=False, mandatory=True) @@ -1272,15 +1276,12 @@ def _list_outputs(self): class DARTELNorm2MNIInputSpec(SPMCommandInputSpec): - template_file = File(exists=True, - desc="DARTEL template", - field='mni_norm.template', copyfile=False, - mandatory=True) - flowfield_files = InputMultiPath(File(exists=True), + template_file = ImageFileSPM(exists=True, copyfile=False, mandatory=True, + desc="DARTEL template", field='mni_norm.template') + flowfield_files = InputMultiPath(ImageFileSPM(exists=True), mandatory=True, desc="DARTEL flow fields u_rc1*", - field='mni_norm.data.subjs.flowfields', - mandatory=True) - apply_to_files = InputMultiPath(File(exists=True), + field='mni_norm.data.subjs.flowfields') + apply_to_files = InputMultiPath(ImageFileSPM(exists=True), desc="Files to apply the transform to", field='mni_norm.data.subjs.images', mandatory=True, copyfile=False) @@ -1370,14 +1371,12 @@ def _list_outputs(self): class CreateWarpedInputSpec(SPMCommandInputSpec): - image_files = InputMultiPath(File(exists=True), + image_files = InputMultiPath(ImageFileSPM(exists=True), mandatory=True, desc="A list of files to be warped", - field='crt_warped.images', copyfile=False, - mandatory=True) - flowfield_files = InputMultiPath(File(exists=True), + field='crt_warped.images', copyfile=False) + flowfield_files = InputMultiPath(ImageFileSPM(exists=True), copyfile=False, desc="DARTEL flow fields u_rc1*", field='crt_warped.flowfields', - copyfile=False, mandatory=True) iterations = traits.Range(low=0, high=9, desc=("The number of iterations: log2(number of " @@ -1440,10 +1439,10 @@ def _list_outputs(self): class ApplyDeformationFieldInputSpec(SPMCommandInputSpec): - in_files = InputMultiPath(File(exists=True), mandatory=True, - field='fnames') + in_files = InputMultiPath(ImageFileSPM(exists=True), + mandatory=True, field='fnames') deformation_field = File(exists=True, mandatory=True, field='comp{1}.def') - reference_volume = File(exists=True, mandatory=True, + reference_volume = ImageFileSPM(exists=True, mandatory=True, field='comp{2}.id.space') interp = traits.Range(low=0, high=7, field='interp', desc='degree of b-spline used for interpolation') @@ -1486,13 +1485,12 @@ def _list_outputs(self): class VBMSegmentInputSpec(SPMCommandInputSpec): in_files = InputMultiPath( - File(exists=True), + ImageFileSPM(exists=True), desc="A list of files to be segmented", field='estwrite.data', copyfile=False, mandatory=True) - tissues = File( - exists=True, field='estwrite.tpm', - desc='tissue probability map') + tissues = ImageFileSPM( + exists=True, field='estwrite.tpm', desc='tissue probability map') gaussians_per_class = traits.Tuple( (2, 2, 2, 3, 4, 2), *([traits.Int()] * 6), usedefault=True, @@ -1518,7 +1516,7 @@ class VBMSegmentInputSpec(SPMCommandInputSpec): spatial_normalization = traits.Enum( 'high', 'low', usedefault=True,) - dartel_template = File( + dartel_template = ImageFileSPM( exists=True, field='estwrite.extopts.dartelwarp.normhigh.darteltpm') use_sanlm_denoising_filter = traits.Range( diff --git a/nipype/interfaces/tests/test_base.py b/nipype/interfaces/tests/test_base.py index e27779ce02..34d1134e42 100644 --- a/nipype/interfaces/tests/test_base.py +++ b/nipype/interfaces/tests/test_base.py @@ -718,3 +718,19 @@ def to_list(x): failed_dict[key] = (value, newval) return failed_dict +def test_ImageFile(): + x = nib.BaseInterface().inputs + + # setup traits + x.add_trait('nifti', nib.ImageFile(types=['nifti1', 'dicom'])) + x.add_trait('anytype', nib.ImageFile()) + x.add_trait('newtype', nib.ImageFile(types=['nifti10'])) + x.add_trait('nocompress', nib.ImageFile(types=['mgh'], + allow_compressed=False)) + + with pytest.raises(nib.TraitError): x.nifti = 'test.mgz' + x.nifti = 'test.nii' + x.anytype = 'test.xml' + with pytest.raises(AttributeError): x.newtype = 'test.nii' + with pytest.raises(nib.TraitError): x.nocompress = 'test.nii.gz' + x.nocompress = 'test.mgh' diff --git a/nipype/interfaces/traits_extension.py b/nipype/interfaces/traits_extension.py index 02342aa96a..0e84c15bce 100644 --- a/nipype/interfaces/traits_extension.py +++ b/nipype/interfaces/traits_extension.py @@ -85,6 +85,11 @@ def validate(self, object, name, value): return validated_value elif os.path.isfile(value): return validated_value + else: + raise TraitError( + args='The trait \'{}\' of {} instance is {}, but the path ' + ' \'{}\' does not exist.'.format(name, class_of(object), + self.info_text, value)) self.error(object, name, value) @@ -173,14 +178,13 @@ def validate(self, object, name, value): if isinstance(value, (str, bytes)): if not self.exists: return value - if os.path.isdir(value): return value else: raise TraitError( args='The trait \'{}\' of {} instance is {}, but the path ' - ' \'{}\' does not exist.'.format(name, class_of(object), - self.info_text, value)) + ' \'{}\' does not exist.'.format(name, + class_of(object), self.info_text, value)) self.error(object, name, value) @@ -218,6 +222,69 @@ def __init__(self, value='', auto_set=False, entries=0, super(Directory, self).__init__(value, auto_set, entries, exists, **metadata) +# lists of tuples +# each element consists of : +# - uncompressed (tuple[0]) extension +# - compressed (tuple[1]) extension +img_fmt_types = { + 'nifti1': [('.nii', '.nii.gz'), + (('.hdr', '.img'), ('.hdr', '.img.gz'))], + 'mgh': [('.mgh', '.mgz'), ('.mgh', '.mgh.gz')], + 'nifti2': [('.nii', '.nii.gz')], + 'cifti2': [('.nii', '.nii.gz')], + 'gifti': [('.gii', '.gii.gz')], + 'dicom': [('.dcm', '.dcm'), ('.IMA', '.IMA'), ('.tar', '.tar.gz')], + 'nrrd': [('.nrrd', 'nrrd'), ('nhdr', 'nhdr')], + 'afni': [('.HEAD', '.HEAD'), ('.BRIK', '.BRIK')] + } + +class ImageFile(File): + """ Defines a trait of specific neuroimaging files """ + + def __init__(self, value='', filter=None, auto_set=False, entries=0, + exists=False, types=[], allow_compressed=True, **metadata): + """ Trait handles neuroimaging files. + + Parameters + ---------- + types : list + Strings of file format types accepted + compressed : boolean + Indicates whether the file format can compressed + """ + self.types = types + self.allow_compressed = allow_compressed + super(ImageFile, self).__init__(value, filter, auto_set, entries, + exists, **metadata) + + def grab_exts(self): + # TODO: file type validation + exts = [] + for fmt in self.types: + if fmt in img_fmt_types: + exts.extend(sum([[u for u in y[0]] if isinstance(y[0], tuple) + else [y[0]] for y in img_fmt_types[fmt]], [])) + if self.allow_compressed: + exts.extend(sum([[u for u in y[-1]] if isinstance(y[-1], + tuple) else [y[-1]] for y in img_fmt_types[fmt]], [])) + else: + raise AttributeError('Information has not been added for format' + ' type {} yet. Supported formats include: ' + '{}'.format(fmt, + ', '.join(img_fmt_types.keys()))) + return list(set(exts)) + + def validate(self, object, name, value): + """ Validates that a specified value is valid for this trait. + """ + validated_value = super(ImageFile, self).validate(object, name, value) + if validated_value and self.types: + self._exts = self.grab_exts() + if not any(validated_value.endswith(x) for x in self._exts): + raise TraitError( + args="{} is not included in allowed types: {}".format( + validated_value, ', '.join(self._exts))) + return validated_value """ The functions that pop-up the Traits GUIs, edit_traits and