From cd8bbfb17ad0342f13c6384cb6aba141d4317785 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Mon, 6 Feb 2023 11:25:50 +1100 Subject: [PATCH 01/75] Created custom callables for different datatypes on cmd Created custom callable classes for handling different datatypes for positional and optional arguments on the command line. Used ArgumentTypeError for exception management instead of ValueError since the former allows allows adding custom error messages. --- lib/mrtrix3/app.py | 84 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 3a2e843c4a..03093cc4bd 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -122,7 +122,7 @@ def _execute(module): #pylint: disable=unused-variable module.usage(CMDLINE) except AttributeError: CMDLINE = None - raise + raise ######################################################################################################################## # Note that everything after this point will only be executed if the script is designed to operate against the library # @@ -1101,6 +1101,88 @@ def _is_option_group(self, group): not group == self._positionals and \ group.title not in ( 'options', 'optional arguments' ) + class TypeBoolean: + def __call__(self, input_value): + true_var = "True" + false_var = "False" + converted_value = None + if input_value.lower() == true_var.lower(): + converted_value = True + elif input_value.lower() == false_var.lower(): + converted_value = False + else: + raise argparse.ArgumentTypeError('Entered value is not of type boolean') + return converted_value + + class TypeIntegerSequence: + def __call__(self, input_value): + seq_elements = input_value.split(',') + int_list = [] + for i in seq_elements: + try: + converted_value = int(i) + int_list.append(converted_value) + except: + raise argparse.ArgumentTypeError('Entered value is not an integer sequence') + return int_list + + class TypeFloatSequence: + def __call__(self, input_value): + seq_elements = input_value.split(',') + float_list = [] + for i in seq_elements: + try: + converted_value = float(i) + float_list.append(converted_value) + except: + argparse.ArgumentTypeError('Entered value is not a float sequence') + return float_list + + class TypeInputDirectory: + def __call__(self, input_value): + if not os.path.exists(input_value): + raise argparse.ArgumentTypeError(input_value + ' does not exist') + elif not os.path.isdir(input_value): + raise argparse.ArgumentTypeError(input_value + ' is not a directory') + else: + return input_value + + class TypeOutputDirectory: + def __call__(self, input_value): + return input_value + + class TypeInputFile: + def __call__(self, input_value): + if not os.path.exists(input_value): + raise argparse.ArgumentTypeError(input_value + ' path does not exist') + elif not os.path.isfile(input_value): + raise argparse.ArgumentTypeError(input_value + ' is not a file') + else: + return input_value + + class TypeOutputFile: + def __call__(self, input_value): + return input_value + + class TypeInputImage: + def __call__(self, input_value): + return input_value + + class TypeOutputImage: + def __call__(self, input_value): + return input_value + + class TypeInputTractogram: + def __call__(self, input_value): + if not input_value.endsWith('.tck'): + raise argparse.ArgumentTypeError(input_value + ' is not a valid track file') + return input_value + + class TypeOutputTractogram: + def __call__(self, input_value): + if not input_value.endsWith('.tck'): + raise argparse.ArgumentTypeError(input_value + ' must use the .tck suffix') + return input_value From 075db52f9f7c88b6505abc799c50d83d654c53b9 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Mon, 6 Feb 2023 11:39:14 +1100 Subject: [PATCH 02/75] Used callable types in commands - mrtrix_cleanup and dwicat Added newly defined callables (in app.py) for each positional and optional command line argument of mrtrix_cleanup and dwicat --- bin/dwicat | 8 +++++--- bin/mrtrix_cleanup | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bin/dwicat b/bin/dwicat index 22a135e71c..f8526ccd15 100755 --- a/bin/dwicat +++ b/bin/dwicat @@ -22,6 +22,8 @@ import json, shutil def usage(cmdline): #pylint: disable=unused-variable + from mrtrix3 import app + cmdline.set_author('Lena Dorfschmidt (ld548@cam.ac.uk) and Jakub Vohryzek (jakub.vohryzek@queens.ox.ac.uk) and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Concatenating multiple DWI series accounting for differential intensity scaling') cmdline.add_description('This script concatenates two or more 4D DWI series, accounting for the ' @@ -29,9 +31,9 @@ def usage(cmdline): #pylint: disable=unused-variable 'This intensity scaling is corrected by determining scaling factors that will ' 'make the overall image intensities in the b=0 volumes of each series approximately ' 'equivalent.') - cmdline.add_argument('inputs', nargs='+', help='Multiple input diffusion MRI series') - cmdline.add_argument('output', help='The output image series (all DWIs concatenated)') - cmdline.add_argument('-mask', metavar='image', help='Provide a binary mask within which image intensities will be matched') + cmdline.add_argument('inputs', nargs='+', type=app.Parser().TypeInputImage(), help='Multiple input diffusion MRI series') + cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output image series (all DWIs concatenated)') + cmdline.add_argument('-mask', metavar='image', type=app.Parser().TypeInputImage(), help='Provide a binary mask within which image intensities will be matched') diff --git a/bin/mrtrix_cleanup b/bin/mrtrix_cleanup index 4b938a1cc9..ee83dc2998 100755 --- a/bin/mrtrix_cleanup +++ b/bin/mrtrix_cleanup @@ -23,12 +23,14 @@ POSTFIXES = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ] def usage(cmdline): #pylint: disable=unused-variable + from mrtrix3 import app + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Clean up residual temporary files & scratch directories from MRtrix3 commands') cmdline.add_description('This script will search the file system at the specified location (and in sub-directories thereof) for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, and attempt to delete them.') cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. Cleanup of such locations should instead be performed explicitly: e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: it may delete temporary items during operation that may lead to unexpected behaviour.') - cmdline.add_argument('path', help='Path from which to commence filesystem search') + cmdline.add_argument('path', type=app.Parser().TypeInputDirectory(), help='Path from which to commence filesystem search') cmdline.add_argument('-test', action='store_true', help='Run script in test mode: will list identified files / directories, but not attempt to delete them') cmdline.add_argument('-failed', metavar='file', nargs=1, help='Write list of items that the script failed to delete to a text file') cmdline.flag_mutually_exclusive_options([ 'test', 'failed' ]) From 8d91a38c27bb2340584b4c3ecb5d3c9556dd6096 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Tue, 7 Feb 2023 12:20:00 +1100 Subject: [PATCH 03/75] Changes done as instructed in the review Updated the logic for TypeBoolean class for consistency with C++ command-line behaviour Added new checks in TypeInputTractogram class for file validation --- lib/mrtrix3/app.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 03093cc4bd..c85fd694d8 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1103,16 +1103,13 @@ def _is_option_group(self, group): class TypeBoolean: def __call__(self, input_value): - true_var = "True" - false_var = "False" - converted_value = None - if input_value.lower() == true_var.lower(): - converted_value = True - elif input_value.lower() == false_var.lower(): - converted_value = False + processed_value = input_value.lower().strip() + if processed_value.lower() == 'true' or processed_value == 'yes': + return True + elif processed_value.lower() == 'false' or processed_value == 'no': + return False else: raise argparse.ArgumentTypeError('Entered value is not of type boolean') - return converted_value class TypeIntegerSequence: def __call__(self, input_value): @@ -1174,15 +1171,21 @@ def __call__(self, input_value): class TypeInputTractogram: def __call__(self, input_value): - if not input_value.endsWith('.tck'): + if not os.path.exists(input_value): + raise argparse.ArgumentTypeError(input_value + ' path does not exist') + elif not os.path.isfile(input_value): + raise argparse.ArgumentTypeError(input_value + ' is not a file') + elif not input_value.endsWith('.tck'): raise argparse.ArgumentTypeError(input_value + ' is not a valid track file') - return input_value + else: + return input_value class TypeOutputTractogram: def __call__(self, input_value): if not input_value.endsWith('.tck'): raise argparse.ArgumentTypeError(input_value + ' must use the .tck suffix') - return input_value + else: + return input_value From ebd36cf76ae31041b5560558d9291aeb2905f6f9 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Tue, 7 Feb 2023 12:24:33 +1100 Subject: [PATCH 04/75] Used callable types in few more commands Added callables for each positional and optional command line argument in dwifslpreproc, dwigradcheck, dwishellmath, labelsgmfix, mask2glass, population_template and responsemean --- bin/dwifslpreproc | 6 +++--- bin/dwigradcheck | 4 ++-- bin/dwishellmath | 4 ++-- bin/labelsgmfix | 9 +++++---- bin/mask2glass | 5 +++-- bin/population_template | 5 +++-- bin/responsemean | 5 +++-- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index 9b982834ea..c5639d6f00 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -50,9 +50,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Zsoldos, E. & Sotiropoulos, S. N. Incorporating outlier detection and replacement into a non-parametric framework for movement and distortion correction of diffusion MR images. NeuroImage, 2016, 141, 556-572', condition='If including "--repol" in -eddy_options input', is_external=True) cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Drobnjak, I.; Zhang, H.; Filippini, N. & Bastiani, M. Towards a comprehensive framework for movement and distortion correction of diffusion MR images: Within volume movement. NeuroImage, 2017, 152, 450-466', condition='If including "--mporder" in -eddy_options input', is_external=True) cmdline.add_citation('Bastiani, M.; Cottaar, M.; Fitzgibbon, S.P.; Suri, S.; Alfaro-Almagro, F.; Sotiropoulos, S.N.; Jbabdi, S.; Andersson, J.L.R. Automated quality control for within and between studies diffusion MRI data using a non-parametric framework for movement and distortion correction. NeuroImage, 2019, 184, 801-812', condition='If using -eddyqc_text or -eddyqc_all option and eddy_quad is installed', is_external=True) - cmdline.add_argument('input', help='The input DWI series to be corrected') - cmdline.add_argument('output', help='The output corrected image series') - cmdline.add_argument('-json_import', metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') + cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input DWI series to be corrected') + cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output corrected image series') + cmdline.add_argument('-json_import', type=app.Parser().TypeInputFile(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') pe_options.add_argument('-pe_dir', metavar=('PE'), help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') pe_options.add_argument('-readout_time', metavar=('time'), type=float, help='Manually specify the total readout time of the input series (in seconds)') diff --git a/bin/dwigradcheck b/bin/dwigradcheck index 664ef3c644..0d90dd02f7 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -29,8 +29,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'More information on mask derivation from DWI data can be found at the following link: \n' 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') cmdline.add_citation('Jeurissen, B.; Leemans, A.; Sijbers, J. Automated correction of improperly rotated diffusion gradient orientations in diffusion weighted MRI. Medical Image Analysis, 2014, 18(7), 953-962') - cmdline.add_argument('input', help='The input DWI series to be checked') - cmdline.add_argument('-mask', metavar='image', help='Provide a mask image within which to seed & constrain tracking') + cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input DWI series to be checked') + cmdline.add_argument('-mask', metavar='image', type=app.Parser().TypeInputImage(), help='Provide a mask image within which to seed & constrain tracking') cmdline.add_argument('-number', type=int, default=10000, help='Set the number of tracks to generate for each test') app.add_dwgrad_export_options(cmdline) diff --git a/bin/dwishellmath b/bin/dwishellmath index 30ce230c01..15bf20431d 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -26,9 +26,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('The output of this command is a 4D image, where ' 'each volume corresponds to a b-value shell (in order of increasing b-value), and ' 'the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell.') - cmdline.add_argument('input', help='The input diffusion MRI series') + cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input diffusion MRI series') cmdline.add_argument('operation', choices=SUPPORTED_OPS, help='The operation to be applied to each shell; this must be one of the following: ' + ', '.join(SUPPORTED_OPS)) - cmdline.add_argument('output', help='The output image series') + cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output image series') cmdline.add_example_usage('To compute the mean diffusion-weighted signal in each b-value shell', 'dwishellmath dwi.mif mean shellmeans.mif') app.add_dwgrad_import_options(cmdline) diff --git a/bin/labelsgmfix b/bin/labelsgmfix index 0b55bd24ef..a04f7e0e12 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -30,15 +30,16 @@ import math, os def usage(cmdline): #pylint: disable=unused-variable + from mrtrix3 import app cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('In a FreeSurfer parcellation image, replace the sub-cortical grey matter structure delineations using FSL FIRST') cmdline.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) cmdline.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. The effects of SIFT on the reproducibility and biological accuracy of the structural connectome. NeuroImage, 2015, 104, 253-265') - cmdline.add_argument('parc', help='The input FreeSurfer parcellation image') - cmdline.add_argument('t1', help='The T1 image to be provided to FIRST') - cmdline.add_argument('lut', help='The lookup table file that the parcellated image is based on') - cmdline.add_argument('output', help='The output parcellation image') + cmdline.add_argument('parc', type=app.Parser().TypeInputImage(), help='The input FreeSurfer parcellation image') + cmdline.add_argument('t1', type=app.Parser().TypeInputImage(), help='The T1 image to be provided to FIRST') + cmdline.add_argument('lut', type=app.Parser().TypeInputFile(), help='The lookup table file that the parcellated image is based on') + cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output parcellation image') cmdline.add_argument('-premasked', action='store_true', default=False, help='Indicate that brain masking has been applied to the T1 input image') cmdline.add_argument('-sgm_amyg_hipp', action='store_true', default=False, help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures, and also replace their estimates with those from FIRST') diff --git a/bin/mask2glass b/bin/mask2glass index bcc9b9ce51..353e63839e 100755 --- a/bin/mask2glass +++ b/bin/mask2glass @@ -16,6 +16,7 @@ # For more details, see http://www.mrtrix.org/. def usage(cmdline): #pylint: disable=unused-variable + from mrtrix3 import app cmdline.set_author('Remika Mito (remika.mito@florey.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Create a glass brain from mask input') cmdline.add_description('The output of this command is a glass brain image, which can be viewed ' @@ -24,8 +25,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'also operate on a floating-point image. One way in which this can be exploited is to compute the mean ' 'of all subject masks within template space, in which case this script will produce a smoother result ' 'than if a binary template mask were to be used as input.') - cmdline.add_argument('input', help='The input mask image') - cmdline.add_argument('output', help='The output glass brain image') + cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input mask image') + cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output glass brain image') cmdline.add_argument('-dilate', type=int, default=2, help='Provide number of passes for dilation step; default = 2') cmdline.add_argument('-scale', type=float, default=2.0, help='Provide resolution upscaling value; default = 2.0') cmdline.add_argument('-smooth', type=float, default=1.0, help='Provide standard deviation of smoothing (in mm); default = 1.0') diff --git a/bin/population_template b/bin/population_template index a0dc7aa918..dcdd2c84f8 100755 --- a/bin/population_template +++ b/bin/population_template @@ -35,12 +35,13 @@ AGGREGATION_MODES = ["mean", "median"] IMAGEEXT = 'mif nii mih mgh mgz img hdr'.split() def usage(cmdline): #pylint: disable=unused-variable + from mrtrix3 import app cmdline.set_author('David Raffelt (david.raffelt@florey.edu.au) & Max Pietsch (maximilian.pietsch@kcl.ac.uk) & Thijs Dhollander (thijs.dhollander@gmail.com)') cmdline.set_synopsis('Generates an unbiased group-average template from a series of images') cmdline.add_description('First a template is optimised with linear registration (rigid and/or affine, both by default), then non-linear registration is used to optimise the template further.') - cmdline.add_argument("input_dir", nargs='+', help='Input directory containing all images used to build the template') - cmdline.add_argument("template", help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') + cmdline.add_argument("input_dir", nargs='+', type=app.Parser().TypeInputDirectory(), help='Input directory containing all images used to build the template') + cmdline.add_argument("template", type=app.Parser().TypeOutputImage(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') options = cmdline.add_argument_group('Multi-contrast options') options.add_argument('-mc_weight_initial_alignment', help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') diff --git a/bin/responsemean b/bin/responsemean index 115e7557d9..b075e6e8cb 100755 --- a/bin/responsemean +++ b/bin/responsemean @@ -21,13 +21,14 @@ import math, os, sys def usage(cmdline): #pylint: disable=unused-variable + from mrtrix3 import app cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') cmdline.set_synopsis('Calculate the mean response function from a set of text files') cmdline.add_description('Example usage: ' + os.path.basename(sys.argv[0]) + ' input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt') cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), as well as the same number of coefficients per line.') cmdline.add_description('As long as the number of unique b-values is identical across all input files, the coefficients will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the ' + os.path.basename(sys.argv[0]) + ' command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied.') - cmdline.add_argument('inputs', help='The input response functions', nargs='+') - cmdline.add_argument('output', help='The output mean response function') + cmdline.add_argument('inputs', type=app.Parser().TypeInputFile(), help='The input response functions file', nargs='+') + cmdline.add_argument('output', type=app.Parser().TypeOutputFile(), help='The output mean response function file') cmdline.add_argument('-legacy', action='store_true', help='Use the legacy behaviour of former command \'average_response\': average response function coefficients directly, without compensating for global magnitude differences between input files') From 298822949ed067860f208933b6fe96a7bbc5a14a Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 7 Feb 2023 13:00:51 +1100 Subject: [PATCH 05/75] Python API: Integrate argument types across commands Co-authored-by: Ankita Sanil --- bin/dwi2response | 8 ++++---- bin/dwibiascorrect | 4 ++-- bin/dwifslpreproc | 18 +++++++++--------- bin/mrtrix_cleanup | 4 ++-- bin/responsemean | 2 +- lib/mrtrix3/_5ttgen/freesurfer.py | 6 +++--- lib/mrtrix3/_5ttgen/fsl.py | 8 ++++---- lib/mrtrix3/_5ttgen/gif.py | 4 ++-- lib/mrtrix3/_5ttgen/hsvs.py | 6 +++--- lib/mrtrix3/dwi2mask/3dautomask.py | 4 ++-- lib/mrtrix3/dwi2mask/ants.py | 6 +++--- lib/mrtrix3/dwi2mask/b02template.py | 8 ++++---- lib/mrtrix3/dwi2mask/consensus.py | 8 ++++---- lib/mrtrix3/dwi2mask/fslbet.py | 6 +++--- lib/mrtrix3/dwi2mask/hdbet.py | 4 ++-- lib/mrtrix3/dwi2mask/legacy.py | 4 ++-- lib/mrtrix3/dwi2mask/mean.py | 4 ++-- lib/mrtrix3/dwi2mask/trace.py | 6 +++--- lib/mrtrix3/dwi2response/dhollander.py | 8 ++++---- lib/mrtrix3/dwi2response/fa.py | 4 ++-- lib/mrtrix3/dwi2response/manual.py | 8 ++++---- lib/mrtrix3/dwi2response/msmt_5tt.py | 12 ++++++------ lib/mrtrix3/dwi2response/tax.py | 4 ++-- lib/mrtrix3/dwi2response/tournier.py | 4 ++-- lib/mrtrix3/dwibiascorrect/ants.py | 4 ++-- lib/mrtrix3/dwibiascorrect/fsl.py | 4 ++-- lib/mrtrix3/dwinormalise/group.py | 10 +++++----- lib/mrtrix3/dwinormalise/individual.py | 6 +++--- 28 files changed, 87 insertions(+), 87 deletions(-) diff --git a/bin/dwi2response b/bin/dwi2response index 85a18606c5..809928ad5f 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -33,10 +33,10 @@ def usage(cmdline): #pylint: disable=unused-variable # General options common_options = cmdline.add_argument_group('General dwi2response options') - common_options.add_argument('-mask', help='Provide an initial mask for response voxel selection') - common_options.add_argument('-voxels', help='Output an image showing the final voxel selection(s)') - common_options.add_argument('-shells', help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly)') - common_options.add_argument('-lmax', help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') + common_options.add_argument('-mask', type=app.Parser.TypeInputImage(), metavar='image', help='Provide an initial mask for response voxel selection') + common_options.add_argument('-voxels', type=app.Parser.TypeOutputImage(), metavar='image', help='Output an image showing the final voxel selection(s)') + common_options.add_argument('-shells', type=app.Parser.TypeFloatSequence(), help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly)') + common_options.add_argument('-lmax', type=app.Parser.TypeIntegerSequence(), help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory diff --git a/bin/dwibiascorrect b/bin/dwibiascorrect index 990b311207..e7e2a7d9d6 100755 --- a/bin/dwibiascorrect +++ b/bin/dwibiascorrect @@ -26,8 +26,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'More information on mask derivation from DWI data can be found at the following link: \n' 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') common_options = cmdline.add_argument_group('Options common to all dwibiascorrect algorithms') - common_options.add_argument('-mask', metavar='image', help='Manually provide a mask image for bias field estimation') - common_options.add_argument('-bias', metavar='image', help='Output the estimated bias field') + common_options.add_argument('-mask', type=app.Parser.TypeInputImage(), metavar='image', help='Manually provide an input mask image for bias field estimation') + common_options.add_argument('-bias', type=app.Prser.TypeOutputImage(), metavar='image', help='Output an image containing the estimated bias field') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index c5639d6f00..42e6f0ade3 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -55,22 +55,22 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_argument('-json_import', type=app.Parser().TypeInputFile(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') pe_options.add_argument('-pe_dir', metavar=('PE'), help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') - pe_options.add_argument('-readout_time', metavar=('time'), type=float, help='Manually specify the total readout time of the input series (in seconds)') + pe_options.add_argument('-readout_time', metavar='time', type=float, help='Manually specify the total readout time of the input series (in seconds)') distcorr_options = cmdline.add_argument_group('Options for achieving correction of susceptibility distortions') - distcorr_options.add_argument('-se_epi', metavar=('image'), help='Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series)') + distcorr_options.add_argument('-se_epi', type=app.Parser.TypeInputImage(), metavar='image', help='Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series)') distcorr_options.add_argument('-align_seepi', action='store_true', help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation, and the DWIs (more information in Description section)') - distcorr_options.add_argument('-topup_options', metavar=('" TopupOptions"'), help='Manually provide additional command-line options to the topup command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to topup)') - distcorr_options.add_argument('-topup_files', metavar=('prefix'), help='Provide files generated by prior execution of the FSL "topup" command to be utilised by eddy') + distcorr_options.add_argument('-topup_options', metavar='" TopupOptions"', help='Manually provide additional command-line options to the topup command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to topup)') + distcorr_options.add_argument('-topup_files', metavar='prefix', help='Provide files generated by prior execution of the FSL "topup" command to be utilised by eddy') cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'se_epi' ], False ) cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'align_seepi' ], False ) cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'topup_options' ], False ) eddy_options = cmdline.add_argument_group('Options for affecting the operation of the FSL "eddy" command') - eddy_options.add_argument('-eddy_mask', metavar=('image'), help='Provide a processing mask to use for eddy, instead of having dwifslpreproc generate one internally using dwi2mask') - eddy_options.add_argument('-eddy_slspec', metavar=('file'), help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') - eddy_options.add_argument('-eddy_options', metavar=('" EddyOptions"'), help='Manually provide additional command-line options to the eddy command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to eddy)') + eddy_options.add_argument('-eddy_mask', type=app.Parser.TypeInputImage(), metavar='image', help='Provide a processing mask to use for eddy, instead of having dwifslpreproc generate one internally using dwi2mask') + eddy_options.add_argument('-eddy_slspec', type=app.Parser.TypeInputFile(), metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') + eddy_options.add_argument('-eddy_options', metavar='" EddyOptions"', help='Manually provide additional command-line options to the eddy command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to eddy)') eddyqc_options = cmdline.add_argument_group('Options for utilising EddyQC') - eddyqc_options.add_argument('-eddyqc_text', metavar=('directory'), help='Copy the various text-based statistical outputs generated by eddy, and the output of eddy_qc (if installed), into an output directory') - eddyqc_options.add_argument('-eddyqc_all', metavar=('directory'), help='Copy ALL outputs generated by eddy (including images), and the output of eddy_qc (if installed), into an output directory') + eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.TypeOutputDirectory(), metavar='directory', help='Copy the various text-based statistical outputs generated by eddy, and the output of eddy_qc (if installed), into an output directory') + eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.TypeOutputDirectory(), metavar='directory', help='Copy ALL outputs generated by eddy (including images), and the output of eddy_qc (if installed), into an output directory') cmdline.flag_mutually_exclusive_options( [ 'eddyqc_text', 'eddyqc_all' ], False ) app.add_dwgrad_export_options(cmdline) app.add_dwgrad_import_options(cmdline) diff --git a/bin/mrtrix_cleanup b/bin/mrtrix_cleanup index ee83dc2998..025f400f98 100755 --- a/bin/mrtrix_cleanup +++ b/bin/mrtrix_cleanup @@ -30,9 +30,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('This script will search the file system at the specified location (and in sub-directories thereof) for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, and attempt to delete them.') cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. Cleanup of such locations should instead be performed explicitly: e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: it may delete temporary items during operation that may lead to unexpected behaviour.') - cmdline.add_argument('path', type=app.Parser().TypeInputDirectory(), help='Path from which to commence filesystem search') + cmdline.add_argument('path', type=app.Parser().TypeInputDirectory(), help='Directory from which to commence filesystem search') cmdline.add_argument('-test', action='store_true', help='Run script in test mode: will list identified files / directories, but not attempt to delete them') - cmdline.add_argument('-failed', metavar='file', nargs=1, help='Write list of items that the script failed to delete to a text file') + cmdline.add_argument('-failed', type=app.Parser.TypeOutputFile(), metavar='file', help='Write list of items that the script failed to delete to a text file') cmdline.flag_mutually_exclusive_options([ 'test', 'failed' ]) diff --git a/bin/responsemean b/bin/responsemean index b075e6e8cb..662123d9fe 100755 --- a/bin/responsemean +++ b/bin/responsemean @@ -27,7 +27,7 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('Example usage: ' + os.path.basename(sys.argv[0]) + ' input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt') cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), as well as the same number of coefficients per line.') cmdline.add_description('As long as the number of unique b-values is identical across all input files, the coefficients will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the ' + os.path.basename(sys.argv[0]) + ' command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied.') - cmdline.add_argument('inputs', type=app.Parser().TypeInputFile(), help='The input response functions file', nargs='+') + cmdline.add_argument('inputs', type=app.Parser().TypeInputFile(), help='The input response function files', nargs='+') cmdline.add_argument('output', type=app.Parser().TypeOutputFile(), help='The output mean response function file') cmdline.add_argument('-legacy', action='store_true', help='Use the legacy behaviour of former command \'average_response\': average response function coefficients directly, without compensating for global magnitude differences between input files') diff --git a/lib/mrtrix3/_5ttgen/freesurfer.py b/lib/mrtrix3/_5ttgen/freesurfer.py index cdfbed3717..7832f31f32 100644 --- a/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/lib/mrtrix3/_5ttgen/freesurfer.py @@ -23,10 +23,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('freesurfer', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate the 5TT image based on a FreeSurfer parcellation image') - parser.add_argument('input', help='The input FreeSurfer parcellation image (any image containing \'aseg\' in its name)') - parser.add_argument('output', help='The output 5TT image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input FreeSurfer parcellation image (any image containing \'aseg\' in its name)') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') options = parser.add_argument_group('Options specific to the \'freesurfer\' algorithm') - options.add_argument('-lut', help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') + options.add_argument('-lut', type=app.Parser.TypeInputFile(), help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') diff --git a/lib/mrtrix3/_5ttgen/fsl.py b/lib/mrtrix3/_5ttgen/fsl.py index e90e7399cb..00aa4bc1c6 100644 --- a/lib/mrtrix3/_5ttgen/fsl.py +++ b/lib/mrtrix3/_5ttgen/fsl.py @@ -27,11 +27,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. IEEE Transactions on Medical Imaging, 2001, 20, 45-57', is_external=True) parser.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) - parser.add_argument('input', help='The input T1-weighted image') - parser.add_argument('output', help='The output 5TT image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input T1-weighted image') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') options = parser.add_argument_group('Options specific to the \'fsl\' algorithm') - options.add_argument('-t2', metavar='', help='Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST') - options.add_argument('-mask', help='Manually provide a brain mask, rather than deriving one in the script') + options.add_argument('-t2', type=app.Parser.TypeInputImage(), metavar='image', help='Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST') + options.add_argument('-mask', type=app.Parser.TypeInputImage(), help='Manually provide a brain mask, rather than deriving one in the script') options.add_argument('-premasked', action='store_true', help='Indicate that brain masking has already been applied to the input image') parser.flag_mutually_exclusive_options( [ 'mask', 'premasked' ] ) diff --git a/lib/mrtrix3/_5ttgen/gif.py b/lib/mrtrix3/_5ttgen/gif.py index 36b4faf699..25c16e5052 100644 --- a/lib/mrtrix3/_5ttgen/gif.py +++ b/lib/mrtrix3/_5ttgen/gif.py @@ -23,8 +23,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('gif', parents=[base_parser]) parser.set_author('Matteo Mancini (m.mancini@ucl.ac.uk)') parser.set_synopsis('Generate the 5TT image based on a Geodesic Information Flow (GIF) segmentation image') - parser.add_argument('input', help='The input Geodesic Information Flow (GIF) segmentation image') - parser.add_argument('output', help='The output 5TT image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input Geodesic Information Flow (GIF) segmentation image') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') def check_output_paths(): #pylint: disable=unused-variable diff --git a/lib/mrtrix3/_5ttgen/hsvs.py b/lib/mrtrix3/_5ttgen/hsvs.py index 1605d63b48..105c44cce7 100644 --- a/lib/mrtrix3/_5ttgen/hsvs.py +++ b/lib/mrtrix3/_5ttgen/hsvs.py @@ -34,9 +34,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('hsvs', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate a 5TT image based on Hybrid Surface and Volume Segmentation (HSVS), using FreeSurfer and FSL tools') - parser.add_argument('input', help='The input FreeSurfer subject directory') - parser.add_argument('output', help='The output 5TT image') - parser.add_argument('-template', help='Provide an image that will form the template for the generated 5TT image') + parser.add_argument('input', type=app.Parser.TypeInputDirectory(), help='The input FreeSurfer subject directory') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') + parser.add_argument('-template', type=app.Parser.TypeInputImage(), help='Provide an image that will form the template for the generated 5TT image') parser.add_argument('-hippocampi', choices=HIPPOCAMPI_CHOICES, help='Select method to be used for hippocampi (& amygdalae) segmentation; options are: ' + ','.join(HIPPOCAMPI_CHOICES)) parser.add_argument('-thalami', choices=THALAMI_CHOICES, help='Select method to be used for thalamic segmentation; options are: ' + ','.join(THALAMI_CHOICES)) parser.add_argument('-white_stem', action='store_true', help='Classify the brainstem as white matter') diff --git a/lib/mrtrix3/dwi2mask/3dautomask.py b/lib/mrtrix3/dwi2mask/3dautomask.py index 59664bb732..e59f1d2820 100644 --- a/lib/mrtrix3/dwi2mask/3dautomask.py +++ b/lib/mrtrix3/dwi2mask/3dautomask.py @@ -25,8 +25,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Ricardo Rios (ricardo.rios@cimat.mx)') parser.set_synopsis('Use AFNI 3dAutomask to derive a brain mask from the DWI mean b=0 image') parser.add_citation('RW Cox. AFNI: Software for analysis and visualization of functional magnetic resonance neuroimages. Computers and Biomedical Research, 29:162-173, 1996.', is_external=True) - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'afni_3dautomask\' algorithm') options.add_argument('-clfrac', type=float, help='Set the \'clip level fraction\', must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger.') options.add_argument('-nograd', action='store_true', help='The program uses a \'gradual\' clip level by default. Add this option to use a fixed clip level.') diff --git a/lib/mrtrix3/dwi2mask/ants.py b/lib/mrtrix3/dwi2mask/ants.py index 48a25475ad..cbdce39dad 100644 --- a/lib/mrtrix3/dwi2mask/ants.py +++ b/lib/mrtrix3/dwi2mask/ants.py @@ -26,10 +26,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use ANTs Brain Extraction to derive a DWI brain mask') parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. A reproducible evaluation of ANTs similarity metric performance in brain image registration. NeuroImage, 2011, 54, 2033-2044', is_external=True) - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') options = parser.add_argument_group('Options specific to the "ants" algorithm') - options.add_argument('-template', metavar='TemplateImage MaskImage', nargs=2, help='Provide the template image and corresponding mask for antsBrainExtraction.sh to use; the template image should be T2-weighted.') + options.add_argument('-template', type=app.Parser.TypeInputImage(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image and corresponding mask for antsBrainExtraction.sh to use; the template image should be T2-weighted.') diff --git a/lib/mrtrix3/dwi2mask/b02template.py b/lib/mrtrix3/dwi2mask/b02template.py index 4c3d1ed073..a3ac35ad7d 100644 --- a/lib/mrtrix3/dwi2mask/b02template.py +++ b/lib/mrtrix3/dwi2mask/b02template.py @@ -65,16 +65,16 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. A reproducible evaluation of ANTs similarity metric performance in brain image registration. NeuroImage, 2011, 54, 2033-2044', condition='If ANTs software is used for registration', is_external=True) - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') options = parser.add_argument_group('Options specific to the "template" algorithm') options.add_argument('-software', choices=SOFTWARES, help='The software to use for template registration; options are: ' + ','.join(SOFTWARES) + '; default is ' + DEFAULT_SOFTWARE) - options.add_argument('-template', metavar='TemplateImage MaskImage', nargs=2, help='Provide the template image to which the input data will be registered, and the mask to be projected to the input image. The template image should be T2-weighted.') + options.add_argument('-template', type=app.Parser.TypeInputImage(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image to which the input data will be registered, and the mask to be projected to the input image. The template image should be T2-weighted.') ants_options = parser.add_argument_group('Options applicable when using the ANTs software for registration') ants_options.add_argument('-ants_options', help='Provide options to be passed to the ANTs registration command (see Description)') fsl_options = parser.add_argument_group('Options applicable when using the FSL software for registration') fsl_options.add_argument('-flirt_options', metavar='" FlirtOptions"', help='Command-line options to pass to the FSL flirt command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to flirt)') - fsl_options.add_argument('-fnirt_config', metavar='FILE', help='Specify a FNIRT configuration file for registration') + fsl_options.add_argument('-fnirt_config', type=app.Parser.TypeInputFile(), metavar='file', help='Specify a FNIRT configuration file for registration') diff --git a/lib/mrtrix3/dwi2mask/consensus.py b/lib/mrtrix3/dwi2mask/consensus.py index 022d09a5d7..90e40d0046 100644 --- a/lib/mrtrix3/dwi2mask/consensus.py +++ b/lib/mrtrix3/dwi2mask/consensus.py @@ -22,12 +22,12 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('consensus', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate a brain mask based on the consensus of all dwi2mask algorithms') - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') options = parser.add_argument_group('Options specific to the "consensus" algorithm') options.add_argument('-algorithms', nargs='+', help='Provide a list of dwi2mask algorithms that are to be utilised') - options.add_argument('-masks', help='Export a 4D image containing the individual algorithm masks') - options.add_argument('-template', metavar='TemplateImage MaskImage', nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') + options.add_argument('-masks', type=app.Parser.TypeOutputImage(), help='Export a 4D image containing the individual algorithm masks') + options.add_argument('-template', type=app.Parser.TypeInputImage(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') options.add_argument('-threshold', type=float, default=DEFAULT_THRESHOLD, help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: ' + str(DEFAULT_THRESHOLD) + ')') diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index 4c67cda5a1..17adc014fe 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -24,12 +24,12 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the FSL Brain Extraction Tool (bet) to generate a brain mask') parser.add_citation('Smith, S. M. Fast robust automated brain extraction. Human Brain Mapping, 2002, 17, 143-155', is_external=True) - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'fslbet\' algorithm') options.add_argument('-bet_f', type=float, help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') options.add_argument('-bet_g', type=float, help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') - options.add_argument('-bet_c', nargs=3, metavar='', help='Centre-of-gravity (voxels not mm) of initial mesh surface') + options.add_argument('-bet_c', type=float, nargs=3, metavar='', help='Centre-of-gravity (voxels not mm) of initial mesh surface') options.add_argument('-bet_r', type=float, help='Head radius (mm not voxels); initial surface sphere is set to half of this') options.add_argument('-rescale', action='store_true', help='Rescale voxel size provided to BET to 1mm isotropic; can improve results for rodent data') diff --git a/lib/mrtrix3/dwi2mask/hdbet.py b/lib/mrtrix3/dwi2mask/hdbet.py index 6b51e397f6..03455ecda6 100644 --- a/lib/mrtrix3/dwi2mask/hdbet.py +++ b/lib/mrtrix3/dwi2mask/hdbet.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use HD-BET to derive a brain mask from the DWI mean b=0 image') parser.add_citation('Isensee F, Schell M, Tursunova I, Brugnara G, Bonekamp D, Neuberger U, Wick A, Schlemmer HP, Heiland S, Wick W, Bendszus M, Maier-Hein KH, Kickingereder P. Automated brain extraction of multi-sequence MRI using artificial neural networks. Hum Brain Mapp. 2019; 1-13. https://doi.org/10.1002/hbm.24750', is_external=True) - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') diff --git a/lib/mrtrix3/dwi2mask/legacy.py b/lib/mrtrix3/dwi2mask/legacy.py index dcba64a179..797d5ef285 100644 --- a/lib/mrtrix3/dwi2mask/legacy.py +++ b/lib/mrtrix3/dwi2mask/legacy.py @@ -23,8 +23,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('legacy', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the legacy MRtrix3 dwi2mask heuristic (based on thresholded trace images)') - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') parser.add_argument('-clean_scale', type=int, default=DEFAULT_CLEAN_SCALE, diff --git a/lib/mrtrix3/dwi2mask/mean.py b/lib/mrtrix3/dwi2mask/mean.py index 30060fdd43..da32b17934 100644 --- a/lib/mrtrix3/dwi2mask/mean.py +++ b/lib/mrtrix3/dwi2mask/mean.py @@ -21,8 +21,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mean', parents=[base_parser]) parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au)') parser.set_synopsis('Generate a mask based on simply averaging all volumes in the DWI series') - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'mean\' algorithm') options.add_argument('-shells', help='Comma separated list of shells to be included in the volume averaging') options.add_argument('-clean_scale', diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index 823d36f95e..696dc87eca 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -23,10 +23,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('trace', parents=[base_parser]) parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('A method to generate a brain mask from trace images of b-value shells') - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'trace\' algorithm') - options.add_argument('-shells', help='Comma separated list of shells used to generate trace-weighted images for masking') + options.add_argument('-shells', type=app.Parser.TypeFloatSequence(), help='Comma-separated list of shells used to generate trace-weighted images for masking') options.add_argument('-clean_scale', type=int, default=DEFAULT_CLEAN_SCALE, diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index faaedecd68..ef01c2d11d 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -31,10 +31,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') parser.add_citation('Dhollander, T.; Mito, R.; Raffelt, D. & Connelly, A. Improved white matter response function estimation for 3-tissue constrained spherical deconvolution. Proc Intl Soc Mag Reson Med, 2019, 555', condition='If -wm_algo option is not used') - parser.add_argument('input', help='Input DWI dataset') - parser.add_argument('out_sfwm', help='Output single-fibre WM response function text file') - parser.add_argument('out_gm', help='Output GM response function text file') - parser.add_argument('out_csf', help='Output CSF response function text file') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='Input DWI dataset') + parser.add_argument('out_sfwm', type=app.Parser.TypeOutputFile(), help='Output single-fibre WM response function text file') + parser.add_argument('out_gm', type=app.Parser.TypeOutputImage(), help='Output GM response function text file') + parser.add_argument('out_csf', type=app.Parser.TypeOutputImage(), help='Output CSF response function text file') options = parser.add_argument_group('Options for the \'dhollander\' algorithm') options.add_argument('-erode', type=int, default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') options.add_argument('-fa', type=float, default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index 5de851ac12..220321f928 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the old FA-threshold heuristic for single-fibre voxel selection and response function estimation') parser.add_citation('Tournier, J.-D.; Calamante, F.; Gadian, D. G. & Connelly, A. Direct estimation of the fiber orientation density function from diffusion-weighted MRI data using spherical deconvolution. NeuroImage, 2004, 23, 1176-1185') - parser.add_argument('input', help='The input DWI') - parser.add_argument('output', help='The output response function text file') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') + parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'fa\' algorithm') options.add_argument('-erode', type=int, default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') options.add_argument('-number', type=int, default=300, help='The number of highest-FA voxels to use') diff --git a/lib/mrtrix3/dwi2response/manual.py b/lib/mrtrix3/dwi2response/manual.py index 0d69b56f12..994e3b9f13 100644 --- a/lib/mrtrix3/dwi2response/manual.py +++ b/lib/mrtrix3/dwi2response/manual.py @@ -23,11 +23,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('manual', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Derive a response function using an input mask image alone (i.e. pre-selected voxels)') - parser.add_argument('input', help='The input DWI') - parser.add_argument('in_voxels', help='Input voxel selection mask') - parser.add_argument('output', help='Output response function text file') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') + parser.add_argument('in_voxels', type=app.Parser.TypeInputImage(), help='Input voxel selection mask') + parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='Output response function text file') options = parser.add_argument_group('Options specific to the \'manual\' algorithm') - options.add_argument('-dirs', help='Manually provide the fibre direction in each voxel (a tensor fit will be used otherwise)') + options.add_argument('-dirs', type=app.Parser.TypeInputImage(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index 30fe810355..9bcbea22cb 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -28,13 +28,13 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Derive MSMT-CSD tissue response functions based on a co-registered five-tissue-type (5TT) image') parser.add_citation('Jeurissen, B.; Tournier, J.-D.; Dhollander, T.; Connelly, A. & Sijbers, J. Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. NeuroImage, 2014, 103, 411-426') - parser.add_argument('input', help='The input DWI') - parser.add_argument('in_5tt', help='Input co-registered 5TT image') - parser.add_argument('out_wm', help='Output WM response text file') - parser.add_argument('out_gm', help='Output GM response text file') - parser.add_argument('out_csf', help='Output CSF response text file') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') + parser.add_argument('in_5tt', type=app.Parser.TypeInputImage(), help='Input co-registered 5TT image') + parser.add_argument('out_wm', type=app.Parser.TypeOutputFile(), help='Output WM response text file') + parser.add_argument('out_gm', type=app.Parser.TypeOutputFile(), help='Output GM response text file') + parser.add_argument('out_csf', type=app.Parser.TypeOutputFile(), help='Output CSF response text file') options = parser.add_argument_group('Options specific to the \'msmt_5tt\' algorithm') - options.add_argument('-dirs', help='Manually provide the fibre direction in each voxel (a tensor fit will be used otherwise)') + options.add_argument('-dirs', type=app.Parser.TypeInputImage(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') options.add_argument('-fa', type=float, default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') options.add_argument('-pvf', type=float, default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, default='tournier', help='dwi2response algorithm to use for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + '; default: tournier)') diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index 33e46f0db7..ddcce2a463 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the Tax et al. (2014) recursive calibration algorithm for single-fibre voxel selection and response function estimation') parser.add_citation('Tax, C. M.; Jeurissen, B.; Vos, S. B.; Viergever, M. A. & Leemans, A. Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. NeuroImage, 2014, 86, 67-80') - parser.add_argument('input', help='The input DWI') - parser.add_argument('output', help='The output response function text file') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') + parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tax\' algorithm') options.add_argument('-peak_ratio', type=float, default=0.1, help='Second-to-first-peak amplitude ratio threshold') options.add_argument('-max_iters', type=int, default=20, help='Maximum number of iterations') diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index 49c31bad0d..8e87a52a91 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the Tournier et al. (2013) iterative algorithm for single-fibre voxel selection and response function estimation') parser.add_citation('Tournier, J.-D.; Calamante, F. & Connelly, A. Determination of the appropriate b-value and number of gradient directions for high-angular-resolution diffusion-weighted imaging. NMR Biomedicine, 2013, 26, 1775-1786') - parser.add_argument('input', help='The input DWI') - parser.add_argument('output', help='The output response function text file') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') + parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tournier\' algorithm') options.add_argument('-number', type=int, default=300, help='Number of single-fibre voxels to use when calculating response function') options.add_argument('-iter_voxels', type=int, default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') diff --git a/lib/mrtrix3/dwibiascorrect/ants.py b/lib/mrtrix3/dwibiascorrect/ants.py index baf8c7c963..c53df71e0a 100644 --- a/lib/mrtrix3/dwibiascorrect/ants.py +++ b/lib/mrtrix3/dwibiascorrect/ants.py @@ -34,8 +34,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable ants_options = parser.add_argument_group('Options for ANTs N4BiasFieldCorrection command') for key in sorted(OPT_N4_BIAS_FIELD_CORRECTION): ants_options.add_argument('-ants.'+key, metavar=OPT_N4_BIAS_FIELD_CORRECTION[key][0], help='N4BiasFieldCorrection option -%s. %s' % (key,OPT_N4_BIAS_FIELD_CORRECTION[key][1])) - parser.add_argument('input', help='The input image series to be corrected') - parser.add_argument('output', help='The output corrected image series') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input image series to be corrected') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output corrected image series') diff --git a/lib/mrtrix3/dwibiascorrect/fsl.py b/lib/mrtrix3/dwibiascorrect/fsl.py index c82ce84707..247aafae4a 100644 --- a/lib/mrtrix3/dwibiascorrect/fsl.py +++ b/lib/mrtrix3/dwibiascorrect/fsl.py @@ -26,8 +26,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. IEEE Transactions on Medical Imaging, 2001, 20, 45-57', is_external=True) parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) parser.add_description('The FSL \'fast\' command only estimates the bias field within a brain mask, and cannot extrapolate this smoothly-varying field beyond the defined mask. As such, this algorithm by necessity introduces a hard masking of the input DWI. Since this attribute may interfere with the purpose of using the command (e.g. correction of a bias field is commonly used to improve brain mask estimation), use of this particular algorithm is generally not recommended.') - parser.add_argument('input', help='The input image series to be corrected') - parser.add_argument('output', help='The output corrected image series') + parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input image series to be corrected') + parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output corrected image series') diff --git a/lib/mrtrix3/dwinormalise/group.py b/lib/mrtrix3/dwinormalise/group.py index 3911833978..a03e5eba67 100644 --- a/lib/mrtrix3/dwinormalise/group.py +++ b/lib/mrtrix3/dwinormalise/group.py @@ -25,11 +25,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Performs a global DWI intensity normalisation on a group of subjects using the median b=0 white matter value as the reference') parser.add_description('The white matter mask is estimated from a population average FA template then warped back to each subject to perform the intensity normalisation. Note that bias field correction should be performed prior to this step.') parser.add_description('All input DWI files must contain an embedded diffusion gradient table; for this reason, these images must all be in either .mif or .mif.gz format.') - parser.add_argument('input_dir', help='The input directory containing all DWI images') - parser.add_argument('mask_dir', help='Input directory containing brain masks, corresponding to one per input image (with the same file name prefix)') - parser.add_argument('output_dir', help='The output directory containing all of the intensity normalised DWI images') - parser.add_argument('fa_template', help='The output population specific FA template, which is threshold to estimate a white matter mask') - parser.add_argument('wm_mask', help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') + parser.add_argument('input_dir', type=app.Parser.TypeInputDirectory(), help='The input directory containing all DWI images') + parser.add_argument('mask_dir', type=app.Parser.TypeInputDirectory(), help='Input directory containing brain masks, corresponding to one per input image (with the same file name prefix)') + parser.add_argument('output_dir', type=app.Parser.TypeOutputDirectory(), help='The output directory containing all of the intensity normalised DWI images') + parser.add_argument('fa_template', type=app.Parser.TypeOutputImage(), help='The output population-specific FA template, which is thresholded to estimate a white matter mask') + parser.add_argument('wm_mask', type=app.Parser.TypeOutputImage(), help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') parser.add_argument('-fa_threshold', default='0.4', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4)') diff --git a/lib/mrtrix3/dwinormalise/individual.py b/lib/mrtrix3/dwinormalise/individual.py index 5ac0500d41..5396ef6243 100644 --- a/lib/mrtrix3/dwinormalise/individual.py +++ b/lib/mrtrix3/dwinormalise/individual.py @@ -26,9 +26,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('individual', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') parser.set_synopsis('Intensity normalise a DWI series based on the b=0 signal within a supplied mask') - parser.add_argument('input_dwi', help='The input DWI series') - parser.add_argument('input_mask', help='The mask within which a reference b=0 intensity will be sampled') - parser.add_argument('output_dwi', help='The output intensity-normalised DWI series') + parser.add_argument('input_dwi', type=app.Parser.TypeInputImage(), help='The input DWI series') + parser.add_argument('input_mask', type=app.Parser.TypeInputImage(), help='The mask within which a reference b=0 intensity will be sampled') + parser.add_argument('output_dwi', type=app.Parser.TypeOutputImage(), help='The output intensity-normalised DWI series') parser.add_argument('-intensity', type=float, default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value (Default: ' + str(DEFAULT_TARGET_INTENSITY) + ')') parser.add_argument('-percentile', type=int, help='Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value') app.add_dwgrad_import_options(parser) From 481462fed9de67bdbed1e0a9105baa3de4c714c3 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Tue, 7 Feb 2023 15:33:16 +1100 Subject: [PATCH 06/75] Changed the syntax as per https://github.com/ankitasanil/mrtrix3/pull/1#issuecomment-1418494241 Used the new syntax as "type=app.Parser.TypeInputImage()" across all Python API commands --- bin/dwicat | 6 +++--- bin/dwifslpreproc | 6 +++--- bin/dwigradcheck | 4 ++-- bin/dwishellmath | 4 ++-- bin/labelsgmfix | 8 ++++---- bin/mask2glass | 4 ++-- bin/mrtrix_cleanup | 2 +- bin/population_template | 4 ++-- bin/responsemean | 4 ++-- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bin/dwicat b/bin/dwicat index f8526ccd15..0231d31490 100755 --- a/bin/dwicat +++ b/bin/dwicat @@ -31,9 +31,9 @@ def usage(cmdline): #pylint: disable=unused-variable 'This intensity scaling is corrected by determining scaling factors that will ' 'make the overall image intensities in the b=0 volumes of each series approximately ' 'equivalent.') - cmdline.add_argument('inputs', nargs='+', type=app.Parser().TypeInputImage(), help='Multiple input diffusion MRI series') - cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output image series (all DWIs concatenated)') - cmdline.add_argument('-mask', metavar='image', type=app.Parser().TypeInputImage(), help='Provide a binary mask within which image intensities will be matched') + cmdline.add_argument('inputs', nargs='+', type=app.Parser.TypeInputImage(), help='Multiple input diffusion MRI series') + cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output image series (all DWIs concatenated)') + cmdline.add_argument('-mask', metavar='image', type=app.Parser.TypeInputImage(), help='Provide a binary mask within which image intensities will be matched') diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index 42e6f0ade3..6c99dbe5dd 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -50,9 +50,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Zsoldos, E. & Sotiropoulos, S. N. Incorporating outlier detection and replacement into a non-parametric framework for movement and distortion correction of diffusion MR images. NeuroImage, 2016, 141, 556-572', condition='If including "--repol" in -eddy_options input', is_external=True) cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Drobnjak, I.; Zhang, H.; Filippini, N. & Bastiani, M. Towards a comprehensive framework for movement and distortion correction of diffusion MR images: Within volume movement. NeuroImage, 2017, 152, 450-466', condition='If including "--mporder" in -eddy_options input', is_external=True) cmdline.add_citation('Bastiani, M.; Cottaar, M.; Fitzgibbon, S.P.; Suri, S.; Alfaro-Almagro, F.; Sotiropoulos, S.N.; Jbabdi, S.; Andersson, J.L.R. Automated quality control for within and between studies diffusion MRI data using a non-parametric framework for movement and distortion correction. NeuroImage, 2019, 184, 801-812', condition='If using -eddyqc_text or -eddyqc_all option and eddy_quad is installed', is_external=True) - cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input DWI series to be corrected') - cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output corrected image series') - cmdline.add_argument('-json_import', type=app.Parser().TypeInputFile(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') + cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series to be corrected') + cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output corrected image series') + cmdline.add_argument('-json_import', type=app.Parser.TypeInputFile(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') pe_options.add_argument('-pe_dir', metavar=('PE'), help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') pe_options.add_argument('-readout_time', metavar='time', type=float, help='Manually specify the total readout time of the input series (in seconds)') diff --git a/bin/dwigradcheck b/bin/dwigradcheck index 0d90dd02f7..1ec976e3c1 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -29,8 +29,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'More information on mask derivation from DWI data can be found at the following link: \n' 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') cmdline.add_citation('Jeurissen, B.; Leemans, A.; Sijbers, J. Automated correction of improperly rotated diffusion gradient orientations in diffusion weighted MRI. Medical Image Analysis, 2014, 18(7), 953-962') - cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input DWI series to be checked') - cmdline.add_argument('-mask', metavar='image', type=app.Parser().TypeInputImage(), help='Provide a mask image within which to seed & constrain tracking') + cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series to be checked') + cmdline.add_argument('-mask', metavar='image', type=app.Parser.TypeInputImage(), help='Provide a mask image within which to seed & constrain tracking') cmdline.add_argument('-number', type=int, default=10000, help='Set the number of tracks to generate for each test') app.add_dwgrad_export_options(cmdline) diff --git a/bin/dwishellmath b/bin/dwishellmath index 15bf20431d..31313a908a 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -26,9 +26,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('The output of this command is a 4D image, where ' 'each volume corresponds to a b-value shell (in order of increasing b-value), and ' 'the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell.') - cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input diffusion MRI series') + cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input diffusion MRI series') cmdline.add_argument('operation', choices=SUPPORTED_OPS, help='The operation to be applied to each shell; this must be one of the following: ' + ', '.join(SUPPORTED_OPS)) - cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output image series') + cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output image series') cmdline.add_example_usage('To compute the mean diffusion-weighted signal in each b-value shell', 'dwishellmath dwi.mif mean shellmeans.mif') app.add_dwgrad_import_options(cmdline) diff --git a/bin/labelsgmfix b/bin/labelsgmfix index a04f7e0e12..926305467b 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -36,10 +36,10 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) cmdline.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. The effects of SIFT on the reproducibility and biological accuracy of the structural connectome. NeuroImage, 2015, 104, 253-265') - cmdline.add_argument('parc', type=app.Parser().TypeInputImage(), help='The input FreeSurfer parcellation image') - cmdline.add_argument('t1', type=app.Parser().TypeInputImage(), help='The T1 image to be provided to FIRST') - cmdline.add_argument('lut', type=app.Parser().TypeInputFile(), help='The lookup table file that the parcellated image is based on') - cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output parcellation image') + cmdline.add_argument('parc', type=app.Parser.TypeInputImage(), help='The input FreeSurfer parcellation image') + cmdline.add_argument('t1', type=app.Parser.TypeInputImage(), help='The T1 image to be provided to FIRST') + cmdline.add_argument('lut', type=app.Parser.TypeInputFile(), help='The lookup table file that the parcellated image is based on') + cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output parcellation image') cmdline.add_argument('-premasked', action='store_true', default=False, help='Indicate that brain masking has been applied to the T1 input image') cmdline.add_argument('-sgm_amyg_hipp', action='store_true', default=False, help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures, and also replace their estimates with those from FIRST') diff --git a/bin/mask2glass b/bin/mask2glass index 353e63839e..bb5b1d3d93 100755 --- a/bin/mask2glass +++ b/bin/mask2glass @@ -25,8 +25,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'also operate on a floating-point image. One way in which this can be exploited is to compute the mean ' 'of all subject masks within template space, in which case this script will produce a smoother result ' 'than if a binary template mask were to be used as input.') - cmdline.add_argument('input', type=app.Parser().TypeInputImage(), help='The input mask image') - cmdline.add_argument('output', type=app.Parser().TypeOutputImage(), help='The output glass brain image') + cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input mask image') + cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output glass brain image') cmdline.add_argument('-dilate', type=int, default=2, help='Provide number of passes for dilation step; default = 2') cmdline.add_argument('-scale', type=float, default=2.0, help='Provide resolution upscaling value; default = 2.0') cmdline.add_argument('-smooth', type=float, default=1.0, help='Provide standard deviation of smoothing (in mm); default = 1.0') diff --git a/bin/mrtrix_cleanup b/bin/mrtrix_cleanup index 025f400f98..a28bafd178 100755 --- a/bin/mrtrix_cleanup +++ b/bin/mrtrix_cleanup @@ -30,7 +30,7 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('This script will search the file system at the specified location (and in sub-directories thereof) for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, and attempt to delete them.') cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. Cleanup of such locations should instead be performed explicitly: e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: it may delete temporary items during operation that may lead to unexpected behaviour.') - cmdline.add_argument('path', type=app.Parser().TypeInputDirectory(), help='Directory from which to commence filesystem search') + cmdline.add_argument('path', type=app.Parser.TypeInputDirectory(), help='Directory from which to commence filesystem search') cmdline.add_argument('-test', action='store_true', help='Run script in test mode: will list identified files / directories, but not attempt to delete them') cmdline.add_argument('-failed', type=app.Parser.TypeOutputFile(), metavar='file', help='Write list of items that the script failed to delete to a text file') cmdline.flag_mutually_exclusive_options([ 'test', 'failed' ]) diff --git a/bin/population_template b/bin/population_template index dcdd2c84f8..c15f5b6042 100755 --- a/bin/population_template +++ b/bin/population_template @@ -40,8 +40,8 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_synopsis('Generates an unbiased group-average template from a series of images') cmdline.add_description('First a template is optimised with linear registration (rigid and/or affine, both by default), then non-linear registration is used to optimise the template further.') - cmdline.add_argument("input_dir", nargs='+', type=app.Parser().TypeInputDirectory(), help='Input directory containing all images used to build the template') - cmdline.add_argument("template", type=app.Parser().TypeOutputImage(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') + cmdline.add_argument("input_dir", nargs='+', type=app.Parser.TypeInputDirectory(), help='Input directory containing all images used to build the template') + cmdline.add_argument("template", type=app.Parser.TypeOutputImage(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') options = cmdline.add_argument_group('Multi-contrast options') options.add_argument('-mc_weight_initial_alignment', help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') diff --git a/bin/responsemean b/bin/responsemean index 662123d9fe..bb8cca9109 100755 --- a/bin/responsemean +++ b/bin/responsemean @@ -27,8 +27,8 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('Example usage: ' + os.path.basename(sys.argv[0]) + ' input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt') cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), as well as the same number of coefficients per line.') cmdline.add_description('As long as the number of unique b-values is identical across all input files, the coefficients will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the ' + os.path.basename(sys.argv[0]) + ' command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied.') - cmdline.add_argument('inputs', type=app.Parser().TypeInputFile(), help='The input response function files', nargs='+') - cmdline.add_argument('output', type=app.Parser().TypeOutputFile(), help='The output mean response function file') + cmdline.add_argument('inputs', type=app.Parser.TypeInputFile(), help='The input response function files', nargs='+') + cmdline.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output mean response function file') cmdline.add_argument('-legacy', action='store_true', help='Use the legacy behaviour of former command \'average_response\': average response function coefficients directly, without compensating for global magnitude differences between input files') From 54bca482ace8b52acd93b374b184cbd99244cbb9 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Tue, 7 Feb 2023 16:18:54 +1100 Subject: [PATCH 07/75] Python API: Used Python list comprehension as per https://github.com/ankitasanil/mrtrix3/pull/1#discussion_r1096932040 Replaced the traditional for loop with list comprehension in TypeIntegerSequence and TypeFloatSequence classes --- lib/mrtrix3/app.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index c85fd694d8..3f5ee7b5d3 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1113,26 +1113,20 @@ def __call__(self, input_value): class TypeIntegerSequence: def __call__(self, input_value): - seq_elements = input_value.split(',') int_list = [] - for i in seq_elements: - try: - converted_value = int(i) - int_list.append(converted_value) - except: - raise argparse.ArgumentTypeError('Entered value is not an integer sequence') + try: + int_list = [int(i) for i in input_value.split(',')] + except (ValueError, NameError) as e: + raise argparse.ArgumentTypeError('Entered value is not an integer sequence') return int_list class TypeFloatSequence: def __call__(self, input_value): - seq_elements = input_value.split(',') float_list = [] - for i in seq_elements: - try: - converted_value = float(i) - float_list.append(converted_value) - except: - argparse.ArgumentTypeError('Entered value is not a float sequence') + try: + float_list = [float(i) for i in input_value.split(',')] + except (ValueError, NameError) as e: + raise argparse.ArgumentTypeError('Entered value is not a float sequence') return float_list class TypeInputDirectory: From cd339306dda6f992207038901518f56651116d1b Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 7 Feb 2023 17:17:15 +1100 Subject: [PATCH 08/75] population_template: Refine cmdline interface --- bin/population_template | 122 +++++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/bin/population_template b/bin/population_template index c15f5b6042..6d86676c8d 100755 --- a/bin/population_template +++ b/bin/population_template @@ -28,11 +28,21 @@ DEFAULT_NL_SCALES = [0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 DEFAULT_NL_NITER = [ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] DEFAULT_NL_LMAX = [ 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4] +DEFAULT_NL_UPDATE_SMOOTH = 2.0 +DEFAULT_NL_DISP_SMOOTH = 1.0 +DEFAULT_NL_GRAD_STEP = 0.5 + REGISTRATION_MODES = ['rigid', 'affine', 'nonlinear', 'rigid_affine', 'rigid_nonlinear', 'affine_nonlinear', 'rigid_affine_nonlinear'] -AGGREGATION_MODES = ["mean", "median"] +AGGREGATION_MODES = ['mean', 'median'] + +LINEAR_ESTIMATORS = ['l1', 'l2', 'lp', 'none'] + +INITIAL_ALIGNMENT = ['mass', 'robust_mass', 'geometric', 'none'] -IMAGEEXT = 'mif nii mih mgh mgz img hdr'.split() +LEAVE_ONE_OUT = ['0', '1', 'auto'] + +IMAGEEXT = ['mif', 'nii', 'mih', 'mgh', 'mgz', 'img', 'hdr'] def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app @@ -44,43 +54,43 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_argument("template", type=app.Parser.TypeOutputImage(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') options = cmdline.add_argument_group('Multi-contrast options') - options.add_argument('-mc_weight_initial_alignment', help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') - options.add_argument('-mc_weight_rigid', help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_affine', help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_nl', help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_initial_alignment', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') + options.add_argument('-mc_weight_rigid', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_affine', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_nl', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') linoptions = cmdline.add_argument_group('Options for the linear registration') linoptions.add_argument('-linear_no_pause', action='store_true', help='Do not pause the script if a linear registration seems implausible') linoptions.add_argument('-linear_no_drift_correction', action='store_true', help='Deactivate correction of template appearance (scale and shear) over iterations') - linoptions.add_argument('-linear_estimator', help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), Default: None (no robust estimator used)') - linoptions.add_argument('-rigid_scale', help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) - linoptions.add_argument('-rigid_lmax', help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) - linoptions.add_argument('-rigid_niter', help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') - linoptions.add_argument('-affine_scale', help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) - linoptions.add_argument('-affine_lmax', help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) - linoptions.add_argument('-affine_niter', help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, default='none', help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), Default: None (no robust estimator used)') + linoptions.add_argument('-rigid_scale', type=app.Parser().TypeFloatSequence(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) + linoptions.add_argument('-rigid_lmax', type=app.Parser().TypeIntegerSequence(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) + linoptions.add_argument('-rigid_niter', type=app.Parser().TypeIntegerSequence(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-affine_scale', type=app.Parser().TypeFloatSequence(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) + linoptions.add_argument('-affine_lmax', type=app.Parser().TypeIntegerSequence(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) + linoptions.add_argument('-affine_niter', type=app.Parser().TypeIntegerSequence(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') nloptions = cmdline.add_argument_group('Options for the non-linear registration') - nloptions.add_argument('-nl_scale', help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) - nloptions.add_argument('-nl_lmax', help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) - nloptions.add_argument('-nl_niter', help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) - nloptions.add_argument('-nl_update_smooth', default='2.0', help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default 2.0 x voxel_size)') - nloptions.add_argument('-nl_disp_smooth', default='1.0', help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default 1.0 x voxel_size)') - nloptions.add_argument('-nl_grad_step', default='0.5', help='The gradient step size for non-linear registration (Default: 0.5)') + nloptions.add_argument('-nl_scale', type=app.Parser().TypeFloatSequence(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) + nloptions.add_argument('-nl_lmax', type=app.Parser().TypeIntegerSequence(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) + nloptions.add_argument('-nl_niter', type=app.Parser().TypeIntegerSequence(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) + nloptions.add_argument('-nl_update_smooth', type=float, default=DEFAULT_NL_UPDATE_SMOOTH, help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_UPDATE_SMOOTH) + ' x voxel_size)') + nloptions.add_argument('-nl_disp_smooth', type=float, default=DEFAULT_NL_DISP_SMOOTH, help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_DISP_SMOOTH) + ' x voxel_size)') + nloptions.add_argument('-nl_grad_step', type=float, default=DEFAULT_NL_GRAD_STEP, help='The gradient step size for non-linear registration (Default: ' + str(DEFAULT_NL_GRAD_STEP) + ')') options = cmdline.add_argument_group('Input, output and general options') - options.add_argument('-type', help='Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"' + x + '"' for x in REGISTRATION_MODES if "_" in x), default='rigid_affine_nonlinear') - options.add_argument('-voxel_size', help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma separated values.') - options.add_argument('-initial_alignment', default='mass', help='Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none".') - options.add_argument('-mask_dir', help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') - options.add_argument('-warp_dir', help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') - options.add_argument('-transformed_dir', help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') - options.add_argument('-linear_transformations_dir', help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') - options.add_argument('-template_mask', help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') + options.add_argument('-type', choices=REGISTRATION_MODES, help='Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"' + x + '"' for x in REGISTRATION_MODES if "_" in x), default='rigid_affine_nonlinear') + options.add_argument('-voxel_size', type=app.Parser().TypeFloatSequence(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') + options.add_argument('-initial_alignment', choices=INITIAL_ALIGNMENT, default='mass', help='Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none".') + options.add_argument('-mask_dir', type=app.Parser().TypeInputDirectory(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') + options.add_argument('-warp_dir', type=app.Parser().TypeOutputDirectory(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') + options.add_argument('-transformed_dir', type=app.Parser().TypeOutputDirectory(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') + options.add_argument('-linear_transformations_dir', type=app.Parser().TypeOutputDirectory(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') + options.add_argument('-template_mask', type=app.Parser().TypeOutputImage(), help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') options.add_argument('-noreorientation', action='store_true', help='Turn off FOD reorientation in mrregister. Reorientation is on by default if the number of volumes in the 4th dimension corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series (i.e. 6, 15, 28, 45, 66 etc)') - options.add_argument('-leave_one_out', help='Register each input image to a template that does not contain that image. Valid choices: 0, 1, auto. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) ') - options.add_argument('-aggregate', help='Measure used to aggregate information from transformed images to the template image. Valid choices: %s. Default: mean' % ', '.join(AGGREGATION_MODES)) - options.add_argument('-aggregation_weights', help='Comma separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') + options.add_argument('-leave_one_out', choices=LEAVE_ONE_OUT, default='auto', help='Register each input image to a template that does not contain that image. Valid choices: ' + ', '.join(LEAVE_ONE_OUT) + '. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) ') + options.add_argument('-aggregate', choices=AGGREGATION_MODES, help='Measure used to aggregate information from transformed images to the template image. Valid choices: %s. Default: mean' % ', '.join(AGGREGATION_MODES)) + options.add_argument('-aggregation_weights', type=app.Parser().TypeInputFile(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') options.add_argument('-nanmask', action='store_true', help='Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. Only works if -mask_dir has been input.') options.add_argument('-copy_input', action='store_true', help='Copy input images and masks into local scratch directory.') options.add_argument('-delete_temporary_files', action='store_true', help='Delete temporary files from scratch directory during template creation.') @@ -268,7 +278,7 @@ class Contrasts: isfinite_count: list of str filenames of images holding (weighted) number of finite-valued voxels across all images - mc_weight_: list of str + mc_weight_: list of floats contrast-specific weight used during initialisation / registration _weight_option: list of str @@ -314,7 +324,7 @@ class Contrasts: else: opt = ["1"] * n_contrasts self.__dict__['mc_weight_%s' % mode] = opt - self.__dict__['%s_weight_option' % mode] = ' -mc_weights '+','.join(opt)+' ' if n_contrasts > 1 else '' + self.__dict__['%s_weight_option' % mode] = ' -mc_weights '+','.join(str(item) for item in opt)+' ' if n_contrasts > 1 else '' if len(self.templates_out) != n_contrasts: raise MRtrixError('number of templates (%i) does not match number of input directories (%i)' % @@ -629,15 +639,9 @@ def execute(): #pylint: disable=unused-variable voxel_size = None if app.ARGS.voxel_size: - voxel_size = app.ARGS.voxel_size.split(',') - if len(voxel_size) == 1: - voxel_size = voxel_size * 3 - try: - if len(voxel_size) != 3: - raise ValueError - [float(v) for v in voxel_size] #pylint: disable=expression-not-assigned - except ValueError as exception: - raise MRtrixError('voxel size needs to be a single or three comma-separated floating point numbers; received: ' + str(app.ARGS.voxel_size)) from exception + voxel_size = app.ARGS.voxel_size + if len(voxel_size) not in [1, 3]: + raise MRtrixError('Voxel size needs to be a single or three comma-separated floating point numbers; received: ' + ',',join(str(item for item in voxel_size))) agg_measure = 'mean' if app.ARGS.aggregate is not None: @@ -761,7 +765,7 @@ def execute(): #pylint: disable=unused-variable # rigid options if app.ARGS.rigid_scale: - rigid_scales = [float(x) for x in app.ARGS.rigid_scale.split(',')] + rigid_scales = app.ARGS.rigid_scale if not dorigid: raise MRtrixError("rigid_scales option set when no rigid registration is performed") else: @@ -769,7 +773,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.rigid_lmax: if not dorigid: raise MRtrixError("rigid_lmax option set when no rigid registration is performed") - rigid_lmax = [int(x) for x in app.ARGS.rigid_lmax.split(',')] + rigid_lmax = app.ARGS.rigid_lmax if do_fod_registration and len(rigid_scales) != len(rigid_lmax): raise MRtrixError('rigid_scales and rigid_lmax schedules are not equal in length: scales stages: %s, lmax stages: %s' % (len(rigid_scales), len(rigid_lmax))) else: @@ -779,7 +783,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.rigid_niter: if not dorigid: raise MRtrixError("rigid_niter specified when no rigid registration is performed") - rigid_niter = [int(x) for x in app.ARGS.rigid_niter.split(',')] + rigid_niter = app.ARGS.rigid_niter if len(rigid_niter) == 1: rigid_niter = rigid_niter * len(rigid_scales) elif len(rigid_scales) != len(rigid_niter): @@ -787,7 +791,7 @@ def execute(): #pylint: disable=unused-variable # affine options if app.ARGS.affine_scale: - affine_scales = [float(x) for x in app.ARGS.affine_scale.split(',')] + affine_scales = app.ARGS.affine_scale if not doaffine: raise MRtrixError("affine_scale option set when no affine registration is performed") else: @@ -795,7 +799,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.affine_lmax: if not doaffine: raise MRtrixError("affine_lmax option set when no affine registration is performed") - affine_lmax = [int(x) for x in app.ARGS.affine_lmax.split(',')] + affine_lmax = app.ARGS.affine_lmax if do_fod_registration and len(affine_scales) != len(affine_lmax): raise MRtrixError('affine_scales and affine_lmax schedules are not equal in length: scales stages: %s, lmax stages: %s' % (len(affine_scales), len(affine_lmax))) else: @@ -805,7 +809,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.affine_niter: if not doaffine: raise MRtrixError("affine_niter specified when no affine registration is performed") - affine_niter = [int(x) for x in app.ARGS.affine_niter.split(',')] + affine_niter = app.ARGS.affine_niter if len(affine_niter) == 1: affine_niter = affine_niter * len(affine_scales) elif len(affine_scales) != len(affine_niter): @@ -843,7 +847,7 @@ def execute(): #pylint: disable=unused-variable if n_contrasts > 1: for cid in range(n_contrasts): app.console('\tcontrast "%s": %s, ' % (cns.suff[cid], cns.names[cid]) + - 'objective weight: %s' % cns.mc_weight_initial_alignment[cid]) + 'objective weight: %s' % ','.join(str(item) for item in cns.mc_weight_initial_alignment[cid])) if dolinear: app.console('-' * 60) @@ -853,9 +857,9 @@ def execute(): #pylint: disable=unused-variable for cid in range(n_contrasts): msg = '\tcontrast "%s": %s' % (cns.suff[cid], cns.names[cid]) if 'rigid' in linear_type: - msg += ', objective weight rigid: %s' % cns.mc_weight_rigid[cid] + msg += ', objective weight rigid: %s' % ','.join(str(item) for item in cns.mc_weight_rigid[cid]) if 'affine' in linear_type: - msg += ', objective weight affine: %s' % cns.mc_weight_affine[cid] + msg += ', objective weight affine: %s' % ','.join(str(item) for item in cns.mc_weight_affine[cid]) app.console(msg) if do_fod_registration: @@ -875,9 +879,9 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.warp_dir: raise MRtrixError('warp_dir specified when no nonlinear registration is performed') else: - nl_scales = [float(x) for x in app.ARGS.nl_scale.split(',')] if app.ARGS.nl_scale else DEFAULT_NL_SCALES - nl_niter = [int(x) for x in app.ARGS.nl_niter.split(',')] if app.ARGS.nl_niter else DEFAULT_NL_NITER - nl_lmax = [int(x) for x in app.ARGS.nl_lmax.split(',')] if app.ARGS.nl_lmax else DEFAULT_NL_LMAX + nl_scales = app.ARGS.nl_scale if app.ARGS.nl_scale else DEFAULT_NL_SCALES + nl_niter = app.ARGS.nl_niter if app.ARGS.nl_niter else DEFAULT_NL_NITER + nl_lmax = app.ARGS.nl_lmax if app.ARGS.nl_lmax else DEFAULT_NL_LMAX if len(nl_scales) != len(nl_niter): raise MRtrixError('nl_scales and nl_niter schedules are not equal in length: scales stages: %s, niter stages: %s' % (len(nl_scales), len(nl_niter))) @@ -887,7 +891,7 @@ def execute(): #pylint: disable=unused-variable app.console('-' * 60) if n_contrasts > 1: for cid in range(n_contrasts): - app.console('\tcontrast "%s": %s, objective weight: %s' % (cns.suff[cid], cns.names[cid], cns.mc_weight_nl[cid])) + app.console('\tcontrast "%s": %s, objective weight: %s' % (cns.suff[cid], cns.names[cid], ','.join(str(item) for item in cns.mc_weight_nl[cid]))) if do_fod_registration: if len(nl_scales) != len(nl_lmax): @@ -1040,9 +1044,9 @@ def execute(): #pylint: disable=unused-variable else: run.command('mrconvert ' + cns.templates[cid] + ' robust/template.mif') if n_contrasts > 1: - cmd = ['mrcalc', inp.ims_path[cid], cns.mc_weight_initial_alignment[cid], '-mult'] + cmd = ['mrcalc', inp.ims_path[cid], ','.join(str(item) for item in cns.mc_weight_initial_alignment[cid]), '-mult'] for cid in range(1, n_contrasts): - cmd += [inp.ims_path[cid], cns.mc_weight_initial_alignment[cid], '-mult', '-add'] + cmd += [inp.ims_path[cid], ','.join(str(item) for item in cns.mc_weight_initial_alignment[cid]), '-mult', '-add'] contrast_weight_option = '' run.command(' '.join(cmd) + ' - | mrfilter - zclean -zlower 3 -zupper 3 robust/image_' + inp.uid + '.mif' @@ -1362,9 +1366,9 @@ def execute(): #pylint: disable=unused-variable ' -nl_warp_full ' + os.path.join('warps_%02i' % level, inp.uid + '.mif') + ' -transformed ' + ' -transformed '.join([inp.ims_transformed[cid] for cid in range(n_contrasts)]) + ' ' + - ' -nl_update_smooth ' + app.ARGS.nl_update_smooth + - ' -nl_disp_smooth ' + app.ARGS.nl_disp_smooth + - ' -nl_grad_step ' + app.ARGS.nl_grad_step + + ' -nl_update_smooth ' + str(app.ARGS.nl_update_smooth) + + ' -nl_disp_smooth ' + str(app.ARGS.nl_disp_smooth) + + ' -nl_grad_step ' + str(app.ARGS.nl_grad_step) + initialise_option + contrast_weight_option + scale_option + From 9f3d624a1758497bd468e25555474825712adea0 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 10 Feb 2023 18:06:34 +1100 Subject: [PATCH 09/75] Change interfaces for robust linear registration estimators Applies to both population-template and mrregister. Makes "none" a valid selection of robust estimator in both cases. --- bin/population_template | 13 +++++-------- cmd/mrregister.cpp | 10 ++++++++-- src/registration/linear.cpp | 3 ++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bin/population_template b/bin/population_template index 6d86676c8d..667e02b7d3 100755 --- a/bin/population_template +++ b/bin/population_template @@ -62,7 +62,7 @@ def usage(cmdline): #pylint: disable=unused-variable linoptions = cmdline.add_argument_group('Options for the linear registration') linoptions.add_argument('-linear_no_pause', action='store_true', help='Do not pause the script if a linear registration seems implausible') linoptions.add_argument('-linear_no_drift_correction', action='store_true', help='Deactivate correction of template appearance (scale and shear) over iterations') - linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, default='none', help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), Default: None (no robust estimator used)') + linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), none (no robust estimator). Default: none.') linoptions.add_argument('-rigid_scale', type=app.Parser().TypeFloatSequence(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) linoptions.add_argument('-rigid_lmax', type=app.Parser().TypeIntegerSequence(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) linoptions.add_argument('-rigid_niter', type=app.Parser().TypeIntegerSequence(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') @@ -662,11 +662,8 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('initial_alignment must be one of ' + " ".join(["mass", "robust_mass", "geometric", "none"]) + " provided: " + str(initial_alignment)) linear_estimator = app.ARGS.linear_estimator - if linear_estimator and not linear_estimator.lower() == 'none': - if not dolinear: - raise MRtrixError('linear_estimator specified when no linear registration is requested') - if linear_estimator not in ["l1", "l2", "lp"]: - raise MRtrixError('linear_estimator must be one of ' + " ".join(["l1", "l2", "lp"]) + " provided: " + str(linear_estimator)) + if linear_estimator is not None and not dolinear: + raise MRtrixError('linear_estimator specified when no linear registration is requested') use_masks = False mask_files = [] @@ -1174,7 +1171,7 @@ def execute(): #pylint: disable=unused-variable os.path.join('linear_transforms_%02i' % (level - 1) if level > 0 else 'linear_transforms_initial', inp.uid + '.txt')) if do_fod_registration: lmax_option = ' -rigid_lmax ' + str(lmax) - if linear_estimator: + if linear_estimator is not None: metric_option = ' -rigid_metric.diff.estimator ' + linear_estimator if app.VERBOSITY >= 2: mrregister_log_option = ' -info -rigid_log ' + os.path.join('log', inp.uid + contrast[cid] + "_" + str(level) + '.log') @@ -1188,7 +1185,7 @@ def execute(): #pylint: disable=unused-variable os.path.join('linear_transforms_%02i' % (level - 1) if level > 0 else 'linear_transforms_initial', inp.uid + '.txt')) if do_fod_registration: lmax_option = ' -affine_lmax ' + str(lmax) - if linear_estimator: + if linear_estimator is not None: metric_option = ' -affine_metric.diff.estimator ' + linear_estimator if write_log: mrregister_log_option = ' -info -affine_log ' + os.path.join('log', inp.uid + contrast[cid] + "_" + str(level) + '.log') diff --git a/cmd/mrregister.cpp b/cmd/mrregister.cpp index df9001abad..78c64d0335 100644 --- a/cmd/mrregister.cpp +++ b/cmd/mrregister.cpp @@ -445,8 +445,11 @@ void run () { case 2: rigid_estimator = Registration::LP; break; - default: + case 3: + rigid_estimator = Registration::None; break; + default: + assert (false); } } @@ -590,8 +593,11 @@ void run () { case 2: affine_estimator = Registration::LP; break; - default: + case 3: + affine_estimator = Registration::None; break; + default: + assert (false); } } diff --git a/src/registration/linear.cpp b/src/registration/linear.cpp index e0bb24790a..3c12bc9e04 100644 --- a/src/registration/linear.cpp +++ b/src/registration/linear.cpp @@ -27,7 +27,7 @@ namespace MR const char* initialisation_rotation_choices[] = { "search", "moments", "none", nullptr }; const char* linear_metric_choices[] = { "diff", "ncc", nullptr }; - const char* linear_robust_estimator_choices[] = { "l1", "l2", "lp", nullptr }; + const char* linear_robust_estimator_choices[] = { "l1", "l2", "lp", "none", nullptr }; const char* linear_optimisation_algo_choices[] = { "bbgd", "gd", nullptr }; const char* optim_algo_names[] = { "BBGD", "GD", nullptr }; @@ -260,6 +260,7 @@ namespace MR "l1 (least absolute: |x|), " "l2 (ordinary least squares), " "lp (least powers: |x|^1.2), " + "none (no robust estimator). " "Default: l2") + Argument ("type").type_choice (linear_robust_estimator_choices) From 51ccc67ddd54e0c692602dffa5fa53475c9c36fe Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Mon, 13 Feb 2023 13:52:32 +1100 Subject: [PATCH 10/75] Python API: Updated class names as per https://github.com/ankitasanil/mrtrix3/pull/1#issuecomment-1420052303 Updated class names across all commands to be in sync with C++ code --- bin/dwi2response | 8 ++--- bin/dwibiascorrect | 4 +-- bin/dwicat | 6 ++-- bin/dwifslpreproc | 16 ++++----- bin/dwigradcheck | 4 +-- bin/dwishellmath | 4 +-- bin/labelsgmfix | 8 ++--- bin/mask2glass | 4 +-- bin/mrtrix_cleanup | 4 +-- bin/population_template | 46 +++++++++++++------------- bin/responsemean | 4 +-- lib/mrtrix3/_5ttgen/freesurfer.py | 6 ++-- lib/mrtrix3/_5ttgen/fsl.py | 8 ++--- lib/mrtrix3/_5ttgen/gif.py | 4 +-- lib/mrtrix3/_5ttgen/hsvs.py | 6 ++-- lib/mrtrix3/app.py | 20 +++++------ lib/mrtrix3/dwi2mask/3dautomask.py | 4 +-- lib/mrtrix3/dwi2mask/ants.py | 6 ++-- lib/mrtrix3/dwi2mask/b02template.py | 8 ++--- lib/mrtrix3/dwi2mask/consensus.py | 8 ++--- lib/mrtrix3/dwi2mask/fslbet.py | 4 +-- lib/mrtrix3/dwi2mask/hdbet.py | 4 +-- lib/mrtrix3/dwi2mask/legacy.py | 4 +-- lib/mrtrix3/dwi2mask/mean.py | 4 +-- lib/mrtrix3/dwi2mask/trace.py | 6 ++-- lib/mrtrix3/dwi2response/dhollander.py | 8 ++--- lib/mrtrix3/dwi2response/fa.py | 4 +-- lib/mrtrix3/dwi2response/manual.py | 8 ++--- lib/mrtrix3/dwi2response/msmt_5tt.py | 12 +++---- lib/mrtrix3/dwi2response/tax.py | 4 +-- lib/mrtrix3/dwi2response/tournier.py | 4 +-- lib/mrtrix3/dwibiascorrect/ants.py | 4 +-- lib/mrtrix3/dwibiascorrect/fsl.py | 4 +-- lib/mrtrix3/dwinormalise/group.py | 10 +++--- lib/mrtrix3/dwinormalise/individual.py | 6 ++-- 35 files changed, 132 insertions(+), 132 deletions(-) diff --git a/bin/dwi2response b/bin/dwi2response index 809928ad5f..2cb4794b37 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -33,10 +33,10 @@ def usage(cmdline): #pylint: disable=unused-variable # General options common_options = cmdline.add_argument_group('General dwi2response options') - common_options.add_argument('-mask', type=app.Parser.TypeInputImage(), metavar='image', help='Provide an initial mask for response voxel selection') - common_options.add_argument('-voxels', type=app.Parser.TypeOutputImage(), metavar='image', help='Output an image showing the final voxel selection(s)') - common_options.add_argument('-shells', type=app.Parser.TypeFloatSequence(), help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly)') - common_options.add_argument('-lmax', type=app.Parser.TypeIntegerSequence(), help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') + common_options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Provide an initial mask for response voxel selection') + common_options.add_argument('-voxels', type=app.Parser.ImageOut(), metavar='image', help='Output an image showing the final voxel selection(s)') + common_options.add_argument('-shells', type=app.Parser.FloatSeq(), help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly)') + common_options.add_argument('-lmax', type=app.Parser.IntSeq(), help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory diff --git a/bin/dwibiascorrect b/bin/dwibiascorrect index e7e2a7d9d6..ba34aa32d4 100755 --- a/bin/dwibiascorrect +++ b/bin/dwibiascorrect @@ -26,8 +26,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'More information on mask derivation from DWI data can be found at the following link: \n' 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') common_options = cmdline.add_argument_group('Options common to all dwibiascorrect algorithms') - common_options.add_argument('-mask', type=app.Parser.TypeInputImage(), metavar='image', help='Manually provide an input mask image for bias field estimation') - common_options.add_argument('-bias', type=app.Prser.TypeOutputImage(), metavar='image', help='Output an image containing the estimated bias field') + common_options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Manually provide an input mask image for bias field estimation') + common_options.add_argument('-bias', type=app.Parser.ImageOut(), metavar='image', help='Output an image containing the estimated bias field') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory diff --git a/bin/dwicat b/bin/dwicat index 0231d31490..9dd8803291 100755 --- a/bin/dwicat +++ b/bin/dwicat @@ -31,9 +31,9 @@ def usage(cmdline): #pylint: disable=unused-variable 'This intensity scaling is corrected by determining scaling factors that will ' 'make the overall image intensities in the b=0 volumes of each series approximately ' 'equivalent.') - cmdline.add_argument('inputs', nargs='+', type=app.Parser.TypeInputImage(), help='Multiple input diffusion MRI series') - cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output image series (all DWIs concatenated)') - cmdline.add_argument('-mask', metavar='image', type=app.Parser.TypeInputImage(), help='Provide a binary mask within which image intensities will be matched') + cmdline.add_argument('inputs', nargs='+', type=app.Parser.ImageIn(), help='Multiple input diffusion MRI series') + cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output image series (all DWIs concatenated)') + cmdline.add_argument('-mask', metavar='image', type=app.Parser.ImageIn(), help='Provide a binary mask within which image intensities will be matched') diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index 6c99dbe5dd..6e50b6deeb 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -50,14 +50,14 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Zsoldos, E. & Sotiropoulos, S. N. Incorporating outlier detection and replacement into a non-parametric framework for movement and distortion correction of diffusion MR images. NeuroImage, 2016, 141, 556-572', condition='If including "--repol" in -eddy_options input', is_external=True) cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Drobnjak, I.; Zhang, H.; Filippini, N. & Bastiani, M. Towards a comprehensive framework for movement and distortion correction of diffusion MR images: Within volume movement. NeuroImage, 2017, 152, 450-466', condition='If including "--mporder" in -eddy_options input', is_external=True) cmdline.add_citation('Bastiani, M.; Cottaar, M.; Fitzgibbon, S.P.; Suri, S.; Alfaro-Almagro, F.; Sotiropoulos, S.N.; Jbabdi, S.; Andersson, J.L.R. Automated quality control for within and between studies diffusion MRI data using a non-parametric framework for movement and distortion correction. NeuroImage, 2019, 184, 801-812', condition='If using -eddyqc_text or -eddyqc_all option and eddy_quad is installed', is_external=True) - cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series to be corrected') - cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output corrected image series') - cmdline.add_argument('-json_import', type=app.Parser.TypeInputFile(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') + cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series to be corrected') + cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') + cmdline.add_argument('-json_import', type=app.Parser.ArgFileIn(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') pe_options.add_argument('-pe_dir', metavar=('PE'), help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') pe_options.add_argument('-readout_time', metavar='time', type=float, help='Manually specify the total readout time of the input series (in seconds)') distcorr_options = cmdline.add_argument_group('Options for achieving correction of susceptibility distortions') - distcorr_options.add_argument('-se_epi', type=app.Parser.TypeInputImage(), metavar='image', help='Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series)') + distcorr_options.add_argument('-se_epi', type=app.Parser.ImageIn(), metavar='image', help='Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series)') distcorr_options.add_argument('-align_seepi', action='store_true', help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation, and the DWIs (more information in Description section)') distcorr_options.add_argument('-topup_options', metavar='" TopupOptions"', help='Manually provide additional command-line options to the topup command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to topup)') distcorr_options.add_argument('-topup_files', metavar='prefix', help='Provide files generated by prior execution of the FSL "topup" command to be utilised by eddy') @@ -65,12 +65,12 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'align_seepi' ], False ) cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'topup_options' ], False ) eddy_options = cmdline.add_argument_group('Options for affecting the operation of the FSL "eddy" command') - eddy_options.add_argument('-eddy_mask', type=app.Parser.TypeInputImage(), metavar='image', help='Provide a processing mask to use for eddy, instead of having dwifslpreproc generate one internally using dwi2mask') - eddy_options.add_argument('-eddy_slspec', type=app.Parser.TypeInputFile(), metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') + eddy_options.add_argument('-eddy_mask', type=app.Parser.ImageIn(), metavar='image', help='Provide a processing mask to use for eddy, instead of having dwifslpreproc generate one internally using dwi2mask') + eddy_options.add_argument('-eddy_slspec', type=app.Parser.ArgFileIn(), metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') eddy_options.add_argument('-eddy_options', metavar='" EddyOptions"', help='Manually provide additional command-line options to the eddy command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to eddy)') eddyqc_options = cmdline.add_argument_group('Options for utilising EddyQC') - eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.TypeOutputDirectory(), metavar='directory', help='Copy the various text-based statistical outputs generated by eddy, and the output of eddy_qc (if installed), into an output directory') - eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.TypeOutputDirectory(), metavar='directory', help='Copy ALL outputs generated by eddy (including images), and the output of eddy_qc (if installed), into an output directory') + eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.ArgDirectoryOut(), metavar='directory', help='Copy the various text-based statistical outputs generated by eddy, and the output of eddy_qc (if installed), into an output directory') + eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.ArgDirectoryOut(), metavar='directory', help='Copy ALL outputs generated by eddy (including images), and the output of eddy_qc (if installed), into an output directory') cmdline.flag_mutually_exclusive_options( [ 'eddyqc_text', 'eddyqc_all' ], False ) app.add_dwgrad_export_options(cmdline) app.add_dwgrad_import_options(cmdline) diff --git a/bin/dwigradcheck b/bin/dwigradcheck index 1ec976e3c1..dc6eaccdec 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -29,8 +29,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'More information on mask derivation from DWI data can be found at the following link: \n' 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') cmdline.add_citation('Jeurissen, B.; Leemans, A.; Sijbers, J. Automated correction of improperly rotated diffusion gradient orientations in diffusion weighted MRI. Medical Image Analysis, 2014, 18(7), 953-962') - cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series to be checked') - cmdline.add_argument('-mask', metavar='image', type=app.Parser.TypeInputImage(), help='Provide a mask image within which to seed & constrain tracking') + cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series to be checked') + cmdline.add_argument('-mask', metavar='image', type=app.Parser.ImageIn(), help='Provide a mask image within which to seed & constrain tracking') cmdline.add_argument('-number', type=int, default=10000, help='Set the number of tracks to generate for each test') app.add_dwgrad_export_options(cmdline) diff --git a/bin/dwishellmath b/bin/dwishellmath index 31313a908a..58d55e3e5a 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -26,9 +26,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('The output of this command is a 4D image, where ' 'each volume corresponds to a b-value shell (in order of increasing b-value), and ' 'the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell.') - cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input diffusion MRI series') + cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input diffusion MRI series') cmdline.add_argument('operation', choices=SUPPORTED_OPS, help='The operation to be applied to each shell; this must be one of the following: ' + ', '.join(SUPPORTED_OPS)) - cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output image series') + cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output image series') cmdline.add_example_usage('To compute the mean diffusion-weighted signal in each b-value shell', 'dwishellmath dwi.mif mean shellmeans.mif') app.add_dwgrad_import_options(cmdline) diff --git a/bin/labelsgmfix b/bin/labelsgmfix index 926305467b..7485ed0e51 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -36,10 +36,10 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) cmdline.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. The effects of SIFT on the reproducibility and biological accuracy of the structural connectome. NeuroImage, 2015, 104, 253-265') - cmdline.add_argument('parc', type=app.Parser.TypeInputImage(), help='The input FreeSurfer parcellation image') - cmdline.add_argument('t1', type=app.Parser.TypeInputImage(), help='The T1 image to be provided to FIRST') - cmdline.add_argument('lut', type=app.Parser.TypeInputFile(), help='The lookup table file that the parcellated image is based on') - cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output parcellation image') + cmdline.add_argument('parc', type=app.Parser.ImageIn(), help='The input FreeSurfer parcellation image') + cmdline.add_argument('t1', type=app.Parser.ImageIn(), help='The T1 image to be provided to FIRST') + cmdline.add_argument('lut', type=app.Parser.ArgFileIn(), help='The lookup table file that the parcellated image is based on') + cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output parcellation image') cmdline.add_argument('-premasked', action='store_true', default=False, help='Indicate that brain masking has been applied to the T1 input image') cmdline.add_argument('-sgm_amyg_hipp', action='store_true', default=False, help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures, and also replace their estimates with those from FIRST') diff --git a/bin/mask2glass b/bin/mask2glass index bb5b1d3d93..193e81319e 100755 --- a/bin/mask2glass +++ b/bin/mask2glass @@ -25,8 +25,8 @@ def usage(cmdline): #pylint: disable=unused-variable 'also operate on a floating-point image. One way in which this can be exploited is to compute the mean ' 'of all subject masks within template space, in which case this script will produce a smoother result ' 'than if a binary template mask were to be used as input.') - cmdline.add_argument('input', type=app.Parser.TypeInputImage(), help='The input mask image') - cmdline.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output glass brain image') + cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input mask image') + cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output glass brain image') cmdline.add_argument('-dilate', type=int, default=2, help='Provide number of passes for dilation step; default = 2') cmdline.add_argument('-scale', type=float, default=2.0, help='Provide resolution upscaling value; default = 2.0') cmdline.add_argument('-smooth', type=float, default=1.0, help='Provide standard deviation of smoothing (in mm); default = 1.0') diff --git a/bin/mrtrix_cleanup b/bin/mrtrix_cleanup index a28bafd178..d82c56d550 100755 --- a/bin/mrtrix_cleanup +++ b/bin/mrtrix_cleanup @@ -30,9 +30,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('This script will search the file system at the specified location (and in sub-directories thereof) for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, and attempt to delete them.') cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. Cleanup of such locations should instead be performed explicitly: e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: it may delete temporary items during operation that may lead to unexpected behaviour.') - cmdline.add_argument('path', type=app.Parser.TypeInputDirectory(), help='Directory from which to commence filesystem search') + cmdline.add_argument('path', type=app.Parser.ArgDirectoryIn(), help='Directory from which to commence filesystem search') cmdline.add_argument('-test', action='store_true', help='Run script in test mode: will list identified files / directories, but not attempt to delete them') - cmdline.add_argument('-failed', type=app.Parser.TypeOutputFile(), metavar='file', help='Write list of items that the script failed to delete to a text file') + cmdline.add_argument('-failed', type=app.Parser.ArgFileOut(), metavar='file', help='Write list of items that the script failed to delete to a text file') cmdline.flag_mutually_exclusive_options([ 'test', 'failed' ]) diff --git a/bin/population_template b/bin/population_template index 667e02b7d3..38999c19bd 100755 --- a/bin/population_template +++ b/bin/population_template @@ -50,47 +50,47 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_synopsis('Generates an unbiased group-average template from a series of images') cmdline.add_description('First a template is optimised with linear registration (rigid and/or affine, both by default), then non-linear registration is used to optimise the template further.') - cmdline.add_argument("input_dir", nargs='+', type=app.Parser.TypeInputDirectory(), help='Input directory containing all images used to build the template') - cmdline.add_argument("template", type=app.Parser.TypeOutputImage(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') + cmdline.add_argument("input_dir", nargs='+', type=app.Parser.ArgDirectoryIn(), help='Input directory containing all images used to build the template') + cmdline.add_argument("template", type=app.Parser.ImageOut(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') options = cmdline.add_argument_group('Multi-contrast options') - options.add_argument('-mc_weight_initial_alignment', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') - options.add_argument('-mc_weight_rigid', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_affine', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_nl', type=app.Parser().TypeFloatSequence(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_initial_alignment', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') + options.add_argument('-mc_weight_rigid', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_affine', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_nl', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') linoptions = cmdline.add_argument_group('Options for the linear registration') linoptions.add_argument('-linear_no_pause', action='store_true', help='Do not pause the script if a linear registration seems implausible') linoptions.add_argument('-linear_no_drift_correction', action='store_true', help='Deactivate correction of template appearance (scale and shear) over iterations') linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), none (no robust estimator). Default: none.') - linoptions.add_argument('-rigid_scale', type=app.Parser().TypeFloatSequence(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) - linoptions.add_argument('-rigid_lmax', type=app.Parser().TypeIntegerSequence(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) - linoptions.add_argument('-rigid_niter', type=app.Parser().TypeIntegerSequence(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') - linoptions.add_argument('-affine_scale', type=app.Parser().TypeFloatSequence(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) - linoptions.add_argument('-affine_lmax', type=app.Parser().TypeIntegerSequence(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) - linoptions.add_argument('-affine_niter', type=app.Parser().TypeIntegerSequence(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-rigid_scale', type=app.Parser().FloatSeq(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) + linoptions.add_argument('-rigid_lmax', type=app.Parser().IntSeq(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) + linoptions.add_argument('-rigid_niter', type=app.Parser().IntSeq(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-affine_scale', type=app.Parser().FloatSeq(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) + linoptions.add_argument('-affine_lmax', type=app.Parser().IntSeq(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) + linoptions.add_argument('-affine_niter', type=app.Parser().IntSeq(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') nloptions = cmdline.add_argument_group('Options for the non-linear registration') - nloptions.add_argument('-nl_scale', type=app.Parser().TypeFloatSequence(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) - nloptions.add_argument('-nl_lmax', type=app.Parser().TypeIntegerSequence(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) - nloptions.add_argument('-nl_niter', type=app.Parser().TypeIntegerSequence(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) + nloptions.add_argument('-nl_scale', type=app.Parser().FloatSeq(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) + nloptions.add_argument('-nl_lmax', type=app.Parser().IntSeq(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) + nloptions.add_argument('-nl_niter', type=app.Parser().IntSeq(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) nloptions.add_argument('-nl_update_smooth', type=float, default=DEFAULT_NL_UPDATE_SMOOTH, help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_UPDATE_SMOOTH) + ' x voxel_size)') nloptions.add_argument('-nl_disp_smooth', type=float, default=DEFAULT_NL_DISP_SMOOTH, help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_DISP_SMOOTH) + ' x voxel_size)') nloptions.add_argument('-nl_grad_step', type=float, default=DEFAULT_NL_GRAD_STEP, help='The gradient step size for non-linear registration (Default: ' + str(DEFAULT_NL_GRAD_STEP) + ')') options = cmdline.add_argument_group('Input, output and general options') options.add_argument('-type', choices=REGISTRATION_MODES, help='Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"' + x + '"' for x in REGISTRATION_MODES if "_" in x), default='rigid_affine_nonlinear') - options.add_argument('-voxel_size', type=app.Parser().TypeFloatSequence(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') + options.add_argument('-voxel_size', type=app.Parser().FloatSeq(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') options.add_argument('-initial_alignment', choices=INITIAL_ALIGNMENT, default='mass', help='Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none".') - options.add_argument('-mask_dir', type=app.Parser().TypeInputDirectory(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') - options.add_argument('-warp_dir', type=app.Parser().TypeOutputDirectory(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') - options.add_argument('-transformed_dir', type=app.Parser().TypeOutputDirectory(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') - options.add_argument('-linear_transformations_dir', type=app.Parser().TypeOutputDirectory(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') - options.add_argument('-template_mask', type=app.Parser().TypeOutputImage(), help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') + options.add_argument('-mask_dir', type=app.Parser().ArgDirectoryIn(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') + options.add_argument('-warp_dir', type=app.Parser().ArgDirectoryOut(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') + options.add_argument('-transformed_dir', type=app.Parser().ArgDirectoryOut(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') + options.add_argument('-linear_transformations_dir', type=app.Parser().ArgDirectoryOut(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') + options.add_argument('-template_mask', type=app.Parser().ImageOut(), help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') options.add_argument('-noreorientation', action='store_true', help='Turn off FOD reorientation in mrregister. Reorientation is on by default if the number of volumes in the 4th dimension corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series (i.e. 6, 15, 28, 45, 66 etc)') options.add_argument('-leave_one_out', choices=LEAVE_ONE_OUT, default='auto', help='Register each input image to a template that does not contain that image. Valid choices: ' + ', '.join(LEAVE_ONE_OUT) + '. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) ') options.add_argument('-aggregate', choices=AGGREGATION_MODES, help='Measure used to aggregate information from transformed images to the template image. Valid choices: %s. Default: mean' % ', '.join(AGGREGATION_MODES)) - options.add_argument('-aggregation_weights', type=app.Parser().TypeInputFile(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') + options.add_argument('-aggregation_weights', type=app.Parser().ArgFileIn(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') options.add_argument('-nanmask', action='store_true', help='Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. Only works if -mask_dir has been input.') options.add_argument('-copy_input', action='store_true', help='Copy input images and masks into local scratch directory.') options.add_argument('-delete_temporary_files', action='store_true', help='Delete temporary files from scratch directory during template creation.') @@ -641,7 +641,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.voxel_size: voxel_size = app.ARGS.voxel_size if len(voxel_size) not in [1, 3]: - raise MRtrixError('Voxel size needs to be a single or three comma-separated floating point numbers; received: ' + ',',join(str(item for item in voxel_size))) + raise MRtrixError('Voxel size needs to be a single or three comma-separated floating point numbers; received: ' + ','.join(str(item for item in voxel_size))) agg_measure = 'mean' if app.ARGS.aggregate is not None: diff --git a/bin/responsemean b/bin/responsemean index bb8cca9109..f4c0e3b153 100755 --- a/bin/responsemean +++ b/bin/responsemean @@ -27,8 +27,8 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('Example usage: ' + os.path.basename(sys.argv[0]) + ' input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt') cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), as well as the same number of coefficients per line.') cmdline.add_description('As long as the number of unique b-values is identical across all input files, the coefficients will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the ' + os.path.basename(sys.argv[0]) + ' command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied.') - cmdline.add_argument('inputs', type=app.Parser.TypeInputFile(), help='The input response function files', nargs='+') - cmdline.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output mean response function file') + cmdline.add_argument('inputs', type=app.Parser.ArgFileIn(), help='The input response function files', nargs='+') + cmdline.add_argument('output', type=app.Parser.ArgFileOut(), help='The output mean response function file') cmdline.add_argument('-legacy', action='store_true', help='Use the legacy behaviour of former command \'average_response\': average response function coefficients directly, without compensating for global magnitude differences between input files') diff --git a/lib/mrtrix3/_5ttgen/freesurfer.py b/lib/mrtrix3/_5ttgen/freesurfer.py index 7832f31f32..995eb433ff 100644 --- a/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/lib/mrtrix3/_5ttgen/freesurfer.py @@ -23,10 +23,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('freesurfer', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate the 5TT image based on a FreeSurfer parcellation image') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input FreeSurfer parcellation image (any image containing \'aseg\' in its name)') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input FreeSurfer parcellation image (any image containing \'aseg\' in its name)') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') options = parser.add_argument_group('Options specific to the \'freesurfer\' algorithm') - options.add_argument('-lut', type=app.Parser.TypeInputFile(), help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') + options.add_argument('-lut', type=app.Parser.ArgFileIn(), help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') diff --git a/lib/mrtrix3/_5ttgen/fsl.py b/lib/mrtrix3/_5ttgen/fsl.py index 00aa4bc1c6..c3c132fe3d 100644 --- a/lib/mrtrix3/_5ttgen/fsl.py +++ b/lib/mrtrix3/_5ttgen/fsl.py @@ -27,11 +27,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. IEEE Transactions on Medical Imaging, 2001, 20, 45-57', is_external=True) parser.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input T1-weighted image') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input T1-weighted image') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') options = parser.add_argument_group('Options specific to the \'fsl\' algorithm') - options.add_argument('-t2', type=app.Parser.TypeInputImage(), metavar='image', help='Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST') - options.add_argument('-mask', type=app.Parser.TypeInputImage(), help='Manually provide a brain mask, rather than deriving one in the script') + options.add_argument('-t2', type=app.Parser.ImageIn(), metavar='image', help='Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST') + options.add_argument('-mask', type=app.Parser.ImageIn(), help='Manually provide a brain mask, rather than deriving one in the script') options.add_argument('-premasked', action='store_true', help='Indicate that brain masking has already been applied to the input image') parser.flag_mutually_exclusive_options( [ 'mask', 'premasked' ] ) diff --git a/lib/mrtrix3/_5ttgen/gif.py b/lib/mrtrix3/_5ttgen/gif.py index 25c16e5052..a0231bebf8 100644 --- a/lib/mrtrix3/_5ttgen/gif.py +++ b/lib/mrtrix3/_5ttgen/gif.py @@ -23,8 +23,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('gif', parents=[base_parser]) parser.set_author('Matteo Mancini (m.mancini@ucl.ac.uk)') parser.set_synopsis('Generate the 5TT image based on a Geodesic Information Flow (GIF) segmentation image') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input Geodesic Information Flow (GIF) segmentation image') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input Geodesic Information Flow (GIF) segmentation image') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') def check_output_paths(): #pylint: disable=unused-variable diff --git a/lib/mrtrix3/_5ttgen/hsvs.py b/lib/mrtrix3/_5ttgen/hsvs.py index 105c44cce7..9fe95b1fa0 100644 --- a/lib/mrtrix3/_5ttgen/hsvs.py +++ b/lib/mrtrix3/_5ttgen/hsvs.py @@ -34,9 +34,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('hsvs', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate a 5TT image based on Hybrid Surface and Volume Segmentation (HSVS), using FreeSurfer and FSL tools') - parser.add_argument('input', type=app.Parser.TypeInputDirectory(), help='The input FreeSurfer subject directory') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output 5TT image') - parser.add_argument('-template', type=app.Parser.TypeInputImage(), help='Provide an image that will form the template for the generated 5TT image') + parser.add_argument('input', type=app.Parser.ArgDirectoryIn(), help='The input FreeSurfer subject directory') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') + parser.add_argument('-template', type=app.Parser.ImageIn(), help='Provide an image that will form the template for the generated 5TT image') parser.add_argument('-hippocampi', choices=HIPPOCAMPI_CHOICES, help='Select method to be used for hippocampi (& amygdalae) segmentation; options are: ' + ','.join(HIPPOCAMPI_CHOICES)) parser.add_argument('-thalami', choices=THALAMI_CHOICES, help='Select method to be used for thalamic segmentation; options are: ' + ','.join(THALAMI_CHOICES)) parser.add_argument('-white_stem', action='store_true', help='Classify the brainstem as white matter') diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 3f5ee7b5d3..ebd113c61e 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1111,7 +1111,7 @@ def __call__(self, input_value): else: raise argparse.ArgumentTypeError('Entered value is not of type boolean') - class TypeIntegerSequence: + class IntSeq: def __call__(self, input_value): int_list = [] try: @@ -1120,7 +1120,7 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError('Entered value is not an integer sequence') return int_list - class TypeFloatSequence: + class FloatSeq: def __call__(self, input_value): float_list = [] try: @@ -1129,7 +1129,7 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError('Entered value is not a float sequence') return float_list - class TypeInputDirectory: + class ArgDirectoryIn: def __call__(self, input_value): if not os.path.exists(input_value): raise argparse.ArgumentTypeError(input_value + ' does not exist') @@ -1138,11 +1138,11 @@ def __call__(self, input_value): else: return input_value - class TypeOutputDirectory: + class ArgDirectoryOut: def __call__(self, input_value): return input_value - class TypeInputFile: + class ArgFileIn: def __call__(self, input_value): if not os.path.exists(input_value): raise argparse.ArgumentTypeError(input_value + ' path does not exist') @@ -1151,19 +1151,19 @@ def __call__(self, input_value): else: return input_value - class TypeOutputFile: + class ArgFileOut: def __call__(self, input_value): return input_value - class TypeInputImage: + class ImageIn: def __call__(self, input_value): return input_value - class TypeOutputImage: + class ImageOut: def __call__(self, input_value): return input_value - class TypeInputTractogram: + class TracksIn: def __call__(self, input_value): if not os.path.exists(input_value): raise argparse.ArgumentTypeError(input_value + ' path does not exist') @@ -1174,7 +1174,7 @@ def __call__(self, input_value): else: return input_value - class TypeOutputTractogram: + class TracksOut: def __call__(self, input_value): if not input_value.endsWith('.tck'): raise argparse.ArgumentTypeError(input_value + ' must use the .tck suffix') diff --git a/lib/mrtrix3/dwi2mask/3dautomask.py b/lib/mrtrix3/dwi2mask/3dautomask.py index e59f1d2820..e4f7bcc3bd 100644 --- a/lib/mrtrix3/dwi2mask/3dautomask.py +++ b/lib/mrtrix3/dwi2mask/3dautomask.py @@ -25,8 +25,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Ricardo Rios (ricardo.rios@cimat.mx)') parser.set_synopsis('Use AFNI 3dAutomask to derive a brain mask from the DWI mean b=0 image') parser.add_citation('RW Cox. AFNI: Software for analysis and visualization of functional magnetic resonance neuroimages. Computers and Biomedical Research, 29:162-173, 1996.', is_external=True) - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'afni_3dautomask\' algorithm') options.add_argument('-clfrac', type=float, help='Set the \'clip level fraction\', must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger.') options.add_argument('-nograd', action='store_true', help='The program uses a \'gradual\' clip level by default. Add this option to use a fixed clip level.') diff --git a/lib/mrtrix3/dwi2mask/ants.py b/lib/mrtrix3/dwi2mask/ants.py index cbdce39dad..64c1649682 100644 --- a/lib/mrtrix3/dwi2mask/ants.py +++ b/lib/mrtrix3/dwi2mask/ants.py @@ -26,10 +26,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use ANTs Brain Extraction to derive a DWI brain mask') parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. A reproducible evaluation of ANTs similarity metric performance in brain image registration. NeuroImage, 2011, 54, 2033-2044', is_external=True) - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the "ants" algorithm') - options.add_argument('-template', type=app.Parser.TypeInputImage(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image and corresponding mask for antsBrainExtraction.sh to use; the template image should be T2-weighted.') + options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image and corresponding mask for antsBrainExtraction.sh to use; the template image should be T2-weighted.') diff --git a/lib/mrtrix3/dwi2mask/b02template.py b/lib/mrtrix3/dwi2mask/b02template.py index a3ac35ad7d..bd02e416e2 100644 --- a/lib/mrtrix3/dwi2mask/b02template.py +++ b/lib/mrtrix3/dwi2mask/b02template.py @@ -65,16 +65,16 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. A reproducible evaluation of ANTs similarity metric performance in brain image registration. NeuroImage, 2011, 54, 2033-2044', condition='If ANTs software is used for registration', is_external=True) - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the "template" algorithm') options.add_argument('-software', choices=SOFTWARES, help='The software to use for template registration; options are: ' + ','.join(SOFTWARES) + '; default is ' + DEFAULT_SOFTWARE) - options.add_argument('-template', type=app.Parser.TypeInputImage(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image to which the input data will be registered, and the mask to be projected to the input image. The template image should be T2-weighted.') + options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image to which the input data will be registered, and the mask to be projected to the input image. The template image should be T2-weighted.') ants_options = parser.add_argument_group('Options applicable when using the ANTs software for registration') ants_options.add_argument('-ants_options', help='Provide options to be passed to the ANTs registration command (see Description)') fsl_options = parser.add_argument_group('Options applicable when using the FSL software for registration') fsl_options.add_argument('-flirt_options', metavar='" FlirtOptions"', help='Command-line options to pass to the FSL flirt command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to flirt)') - fsl_options.add_argument('-fnirt_config', type=app.Parser.TypeInputFile(), metavar='file', help='Specify a FNIRT configuration file for registration') + fsl_options.add_argument('-fnirt_config', type=app.Parser.ArgFileIn(), metavar='file', help='Specify a FNIRT configuration file for registration') diff --git a/lib/mrtrix3/dwi2mask/consensus.py b/lib/mrtrix3/dwi2mask/consensus.py index 90e40d0046..372f913d2a 100644 --- a/lib/mrtrix3/dwi2mask/consensus.py +++ b/lib/mrtrix3/dwi2mask/consensus.py @@ -22,12 +22,12 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('consensus', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate a brain mask based on the consensus of all dwi2mask algorithms') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the "consensus" algorithm') options.add_argument('-algorithms', nargs='+', help='Provide a list of dwi2mask algorithms that are to be utilised') - options.add_argument('-masks', type=app.Parser.TypeOutputImage(), help='Export a 4D image containing the individual algorithm masks') - options.add_argument('-template', type=app.Parser.TypeInputImage(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') + options.add_argument('-masks', type=app.Parser.ImageOut(), help='Export a 4D image containing the individual algorithm masks') + options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') options.add_argument('-threshold', type=float, default=DEFAULT_THRESHOLD, help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: ' + str(DEFAULT_THRESHOLD) + ')') diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index 17adc014fe..4a8cee3995 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the FSL Brain Extraction Tool (bet) to generate a brain mask') parser.add_citation('Smith, S. M. Fast robust automated brain extraction. Human Brain Mapping, 2002, 17, 143-155', is_external=True) - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'fslbet\' algorithm') options.add_argument('-bet_f', type=float, help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') options.add_argument('-bet_g', type=float, help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') diff --git a/lib/mrtrix3/dwi2mask/hdbet.py b/lib/mrtrix3/dwi2mask/hdbet.py index 03455ecda6..a70be49d1a 100644 --- a/lib/mrtrix3/dwi2mask/hdbet.py +++ b/lib/mrtrix3/dwi2mask/hdbet.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use HD-BET to derive a brain mask from the DWI mean b=0 image') parser.add_citation('Isensee F, Schell M, Tursunova I, Brugnara G, Bonekamp D, Neuberger U, Wick A, Schlemmer HP, Heiland S, Wick W, Bendszus M, Maier-Hein KH, Kickingereder P. Automated brain extraction of multi-sequence MRI using artificial neural networks. Hum Brain Mapp. 2019; 1-13. https://doi.org/10.1002/hbm.24750', is_external=True) - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') diff --git a/lib/mrtrix3/dwi2mask/legacy.py b/lib/mrtrix3/dwi2mask/legacy.py index 797d5ef285..113e51c4dc 100644 --- a/lib/mrtrix3/dwi2mask/legacy.py +++ b/lib/mrtrix3/dwi2mask/legacy.py @@ -23,8 +23,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('legacy', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the legacy MRtrix3 dwi2mask heuristic (based on thresholded trace images)') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') parser.add_argument('-clean_scale', type=int, default=DEFAULT_CLEAN_SCALE, diff --git a/lib/mrtrix3/dwi2mask/mean.py b/lib/mrtrix3/dwi2mask/mean.py index da32b17934..042dc5ba8b 100644 --- a/lib/mrtrix3/dwi2mask/mean.py +++ b/lib/mrtrix3/dwi2mask/mean.py @@ -21,8 +21,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mean', parents=[base_parser]) parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au)') parser.set_synopsis('Generate a mask based on simply averaging all volumes in the DWI series') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'mean\' algorithm') options.add_argument('-shells', help='Comma separated list of shells to be included in the volume averaging') options.add_argument('-clean_scale', diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index 696dc87eca..6f6e3d6dfb 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -23,10 +23,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('trace', parents=[base_parser]) parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('A method to generate a brain mask from trace images of b-value shells') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'trace\' algorithm') - options.add_argument('-shells', type=app.Parser.TypeFloatSequence(), help='Comma-separated list of shells used to generate trace-weighted images for masking') + options.add_argument('-shells', type=app.Parser.FloatSeq(), help='Comma-separated list of shells used to generate trace-weighted images for masking') options.add_argument('-clean_scale', type=int, default=DEFAULT_CLEAN_SCALE, diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index ef01c2d11d..c146f57a02 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -31,10 +31,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') parser.add_citation('Dhollander, T.; Mito, R.; Raffelt, D. & Connelly, A. Improved white matter response function estimation for 3-tissue constrained spherical deconvolution. Proc Intl Soc Mag Reson Med, 2019, 555', condition='If -wm_algo option is not used') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='Input DWI dataset') - parser.add_argument('out_sfwm', type=app.Parser.TypeOutputFile(), help='Output single-fibre WM response function text file') - parser.add_argument('out_gm', type=app.Parser.TypeOutputImage(), help='Output GM response function text file') - parser.add_argument('out_csf', type=app.Parser.TypeOutputImage(), help='Output CSF response function text file') + parser.add_argument('input', type=app.Parser.ImageIn(), help='Input DWI dataset') + parser.add_argument('out_sfwm', type=app.Parser.ArgFileOut(), help='Output single-fibre WM response function text file') + parser.add_argument('out_gm', type=app.Parser.ImageOut(), help='Output GM response function text file') + parser.add_argument('out_csf', type=app.Parser.ImageOut(), help='Output CSF response function text file') options = parser.add_argument_group('Options for the \'dhollander\' algorithm') options.add_argument('-erode', type=int, default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') options.add_argument('-fa', type=float, default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index 220321f928..fd17765f3f 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the old FA-threshold heuristic for single-fibre voxel selection and response function estimation') parser.add_citation('Tournier, J.-D.; Calamante, F.; Gadian, D. G. & Connelly, A. Direct estimation of the fiber orientation density function from diffusion-weighted MRI data using spherical deconvolution. NeuroImage, 2004, 23, 1176-1185') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') - parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output response function text file') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') + parser.add_argument('output', type=app.Parser.ArgFileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'fa\' algorithm') options.add_argument('-erode', type=int, default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') options.add_argument('-number', type=int, default=300, help='The number of highest-FA voxels to use') diff --git a/lib/mrtrix3/dwi2response/manual.py b/lib/mrtrix3/dwi2response/manual.py index 994e3b9f13..af2ce7a5de 100644 --- a/lib/mrtrix3/dwi2response/manual.py +++ b/lib/mrtrix3/dwi2response/manual.py @@ -23,11 +23,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('manual', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Derive a response function using an input mask image alone (i.e. pre-selected voxels)') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') - parser.add_argument('in_voxels', type=app.Parser.TypeInputImage(), help='Input voxel selection mask') - parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='Output response function text file') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') + parser.add_argument('in_voxels', type=app.Parser.ImageIn(), help='Input voxel selection mask') + parser.add_argument('output', type=app.Parser.ArgFileOut(), help='Output response function text file') options = parser.add_argument_group('Options specific to the \'manual\' algorithm') - options.add_argument('-dirs', type=app.Parser.TypeInputImage(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') + options.add_argument('-dirs', type=app.Parser.ImageIn(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index 9bcbea22cb..4e92dfcd37 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -28,13 +28,13 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Derive MSMT-CSD tissue response functions based on a co-registered five-tissue-type (5TT) image') parser.add_citation('Jeurissen, B.; Tournier, J.-D.; Dhollander, T.; Connelly, A. & Sijbers, J. Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. NeuroImage, 2014, 103, 411-426') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') - parser.add_argument('in_5tt', type=app.Parser.TypeInputImage(), help='Input co-registered 5TT image') - parser.add_argument('out_wm', type=app.Parser.TypeOutputFile(), help='Output WM response text file') - parser.add_argument('out_gm', type=app.Parser.TypeOutputFile(), help='Output GM response text file') - parser.add_argument('out_csf', type=app.Parser.TypeOutputFile(), help='Output CSF response text file') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') + parser.add_argument('in_5tt', type=app.Parser.ImageIn(), help='Input co-registered 5TT image') + parser.add_argument('out_wm', type=app.Parser.ArgFileOut(), help='Output WM response text file') + parser.add_argument('out_gm', type=app.Parser.ArgFileOut(), help='Output GM response text file') + parser.add_argument('out_csf', type=app.Parser.ArgFileOut(), help='Output CSF response text file') options = parser.add_argument_group('Options specific to the \'msmt_5tt\' algorithm') - options.add_argument('-dirs', type=app.Parser.TypeInputImage(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') + options.add_argument('-dirs', type=app.Parser.ImageIn(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') options.add_argument('-fa', type=float, default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') options.add_argument('-pvf', type=float, default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, default='tournier', help='dwi2response algorithm to use for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + '; default: tournier)') diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index ddcce2a463..c7024e219f 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the Tax et al. (2014) recursive calibration algorithm for single-fibre voxel selection and response function estimation') parser.add_citation('Tax, C. M.; Jeurissen, B.; Vos, S. B.; Viergever, M. A. & Leemans, A. Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. NeuroImage, 2014, 86, 67-80') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') - parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output response function text file') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') + parser.add_argument('output', type=app.Parser.ArgFileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tax\' algorithm') options.add_argument('-peak_ratio', type=float, default=0.1, help='Second-to-first-peak amplitude ratio threshold') options.add_argument('-max_iters', type=int, default=20, help='Maximum number of iterations') diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index 8e87a52a91..0dbe589df6 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -24,8 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the Tournier et al. (2013) iterative algorithm for single-fibre voxel selection and response function estimation') parser.add_citation('Tournier, J.-D.; Calamante, F. & Connelly, A. Determination of the appropriate b-value and number of gradient directions for high-angular-resolution diffusion-weighted imaging. NMR Biomedicine, 2013, 26, 1775-1786') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input DWI') - parser.add_argument('output', type=app.Parser.TypeOutputFile(), help='The output response function text file') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') + parser.add_argument('output', type=app.Parser.ArgFileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tournier\' algorithm') options.add_argument('-number', type=int, default=300, help='Number of single-fibre voxels to use when calculating response function') options.add_argument('-iter_voxels', type=int, default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') diff --git a/lib/mrtrix3/dwibiascorrect/ants.py b/lib/mrtrix3/dwibiascorrect/ants.py index c53df71e0a..d3b2e567f5 100644 --- a/lib/mrtrix3/dwibiascorrect/ants.py +++ b/lib/mrtrix3/dwibiascorrect/ants.py @@ -34,8 +34,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable ants_options = parser.add_argument_group('Options for ANTs N4BiasFieldCorrection command') for key in sorted(OPT_N4_BIAS_FIELD_CORRECTION): ants_options.add_argument('-ants.'+key, metavar=OPT_N4_BIAS_FIELD_CORRECTION[key][0], help='N4BiasFieldCorrection option -%s. %s' % (key,OPT_N4_BIAS_FIELD_CORRECTION[key][1])) - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input image series to be corrected') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output corrected image series') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input image series to be corrected') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') diff --git a/lib/mrtrix3/dwibiascorrect/fsl.py b/lib/mrtrix3/dwibiascorrect/fsl.py index 247aafae4a..95842be1d5 100644 --- a/lib/mrtrix3/dwibiascorrect/fsl.py +++ b/lib/mrtrix3/dwibiascorrect/fsl.py @@ -26,8 +26,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. IEEE Transactions on Medical Imaging, 2001, 20, 45-57', is_external=True) parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) parser.add_description('The FSL \'fast\' command only estimates the bias field within a brain mask, and cannot extrapolate this smoothly-varying field beyond the defined mask. As such, this algorithm by necessity introduces a hard masking of the input DWI. Since this attribute may interfere with the purpose of using the command (e.g. correction of a bias field is commonly used to improve brain mask estimation), use of this particular algorithm is generally not recommended.') - parser.add_argument('input', type=app.Parser.TypeInputImage(), help='The input image series to be corrected') - parser.add_argument('output', type=app.Parser.TypeOutputImage(), help='The output corrected image series') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input image series to be corrected') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') diff --git a/lib/mrtrix3/dwinormalise/group.py b/lib/mrtrix3/dwinormalise/group.py index a03e5eba67..7e6f1e8e63 100644 --- a/lib/mrtrix3/dwinormalise/group.py +++ b/lib/mrtrix3/dwinormalise/group.py @@ -25,11 +25,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Performs a global DWI intensity normalisation on a group of subjects using the median b=0 white matter value as the reference') parser.add_description('The white matter mask is estimated from a population average FA template then warped back to each subject to perform the intensity normalisation. Note that bias field correction should be performed prior to this step.') parser.add_description('All input DWI files must contain an embedded diffusion gradient table; for this reason, these images must all be in either .mif or .mif.gz format.') - parser.add_argument('input_dir', type=app.Parser.TypeInputDirectory(), help='The input directory containing all DWI images') - parser.add_argument('mask_dir', type=app.Parser.TypeInputDirectory(), help='Input directory containing brain masks, corresponding to one per input image (with the same file name prefix)') - parser.add_argument('output_dir', type=app.Parser.TypeOutputDirectory(), help='The output directory containing all of the intensity normalised DWI images') - parser.add_argument('fa_template', type=app.Parser.TypeOutputImage(), help='The output population-specific FA template, which is thresholded to estimate a white matter mask') - parser.add_argument('wm_mask', type=app.Parser.TypeOutputImage(), help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') + parser.add_argument('input_dir', type=app.Parser.ArgDirectoryIn(), help='The input directory containing all DWI images') + parser.add_argument('mask_dir', type=app.Parser.ArgDirectoryIn(), help='Input directory containing brain masks, corresponding to one per input image (with the same file name prefix)') + parser.add_argument('output_dir', type=app.Parser.ArgDirectoryOut(), help='The output directory containing all of the intensity normalised DWI images') + parser.add_argument('fa_template', type=app.Parser.ImageOut(), help='The output population-specific FA template, which is thresholded to estimate a white matter mask') + parser.add_argument('wm_mask', type=app.Parser.ImageOut(), help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') parser.add_argument('-fa_threshold', default='0.4', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4)') diff --git a/lib/mrtrix3/dwinormalise/individual.py b/lib/mrtrix3/dwinormalise/individual.py index 5396ef6243..92716de89c 100644 --- a/lib/mrtrix3/dwinormalise/individual.py +++ b/lib/mrtrix3/dwinormalise/individual.py @@ -26,9 +26,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('individual', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') parser.set_synopsis('Intensity normalise a DWI series based on the b=0 signal within a supplied mask') - parser.add_argument('input_dwi', type=app.Parser.TypeInputImage(), help='The input DWI series') - parser.add_argument('input_mask', type=app.Parser.TypeInputImage(), help='The mask within which a reference b=0 intensity will be sampled') - parser.add_argument('output_dwi', type=app.Parser.TypeOutputImage(), help='The output intensity-normalised DWI series') + parser.add_argument('input_dwi', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('input_mask', type=app.Parser.ImageIn(), help='The mask within which a reference b=0 intensity will be sampled') + parser.add_argument('output_dwi', type=app.Parser.ImageOut(), help='The output intensity-normalised DWI series') parser.add_argument('-intensity', type=float, default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value (Default: ' + str(DEFAULT_TARGET_INTENSITY) + ')') parser.add_argument('-percentile', type=int, help='Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value') app.add_dwgrad_import_options(parser) From 47fec95ef66b99f2f2fcbf8d071a13f1c7560503 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Mon, 13 Feb 2023 14:33:46 +1100 Subject: [PATCH 11/75] Modifications in existing code to handle new argument types --- bin/dwi2response | 4 ++-- lib/mrtrix3/dwi2mask/fslbet.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/dwi2response b/bin/dwi2response index 2cb4794b37..80cbbf4ed1 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -62,7 +62,7 @@ def execute(): #pylint: disable=unused-variable # Sanitise some inputs, and get ready for data import if app.ARGS.lmax: try: - lmax = [ int(x) for x in app.ARGS.lmax.split(',') ] + lmax = app.ARGS.lmax if any(lmax_value%2 for lmax_value in lmax): raise MRtrixError('Value of lmax must be even') except ValueError as exception: @@ -72,7 +72,7 @@ def execute(): #pylint: disable=unused-variable shells_option = '' if app.ARGS.shells: try: - shells_values = [ int(round(float(x))) for x in app.ARGS.shells.split(',') ] + shells_values = [ int(round(x)) for x in app.ARGS.shells ] except ValueError as exception: raise MRtrixError('-shells option should provide a comma-separated list of b-values') from exception if alg.needs_single_shell() and not len(shells_values) == 1: diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index 4a8cee3995..36718f146f 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -67,7 +67,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.bet_r is not None: cmd_string += ' -r ' + str(app.ARGS.bet_r) if app.ARGS.bet_c is not None: - cmd_string += ' -c ' + ' '.join(app.ARGS.bet_c) + cmd_string += ' -c ' + str(app.ARGS.bet_c) # Running BET command run.command(cmd_string) From b72c2116275ba2661d0241d6032f62b3b8ae3c3f Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Tue, 14 Feb 2023 16:18:41 +1100 Subject: [PATCH 12/75] Modifications in existing code to handle new argument types (contd.) --- bin/dwi2response | 2 +- bin/population_template | 2 +- lib/mrtrix3/dwi2mask/fslbet.py | 4 ++-- lib/mrtrix3/dwi2mask/trace.py | 2 +- lib/mrtrix3/dwi2response/dhollander.py | 2 +- lib/mrtrix3/dwi2response/fa.py | 2 +- lib/mrtrix3/dwi2response/manual.py | 2 +- lib/mrtrix3/dwi2response/msmt_5tt.py | 2 +- lib/mrtrix3/dwi2response/tax.py | 2 +- lib/mrtrix3/dwi2response/tournier.py | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bin/dwi2response b/bin/dwi2response index 80cbbf4ed1..2e17e2b3a8 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -77,7 +77,7 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('-shells option should provide a comma-separated list of b-values') from exception if alg.needs_single_shell() and not len(shells_values) == 1: raise MRtrixError('Can only specify a single b-value shell for single-shell algorithms') - shells_option = ' -shells ' + app.ARGS.shells + shells_option = ' -shells ' + ','.join(str(item) for item in app.ARGS.shells) singleshell_option = '' if alg.needs_single_shell(): singleshell_option = ' -singleshell -no_bzero' diff --git a/bin/population_template b/bin/population_template index 38999c19bd..c6662b016a 100755 --- a/bin/population_template +++ b/bin/population_template @@ -641,7 +641,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.voxel_size: voxel_size = app.ARGS.voxel_size if len(voxel_size) not in [1, 3]: - raise MRtrixError('Voxel size needs to be a single or three comma-separated floating point numbers; received: ' + ','.join(str(item for item in voxel_size))) + raise MRtrixError('Voxel size needs to be a single or three comma-separated floating point numbers; received: ' + ','.join(str(item) for item in voxel_size)) agg_measure = 'mean' if app.ARGS.aggregate is not None: diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index 36718f146f..7c6f2da72a 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -29,7 +29,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the \'fslbet\' algorithm') options.add_argument('-bet_f', type=float, help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') options.add_argument('-bet_g', type=float, help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') - options.add_argument('-bet_c', type=float, nargs=3, metavar='', help='Centre-of-gravity (voxels not mm) of initial mesh surface') + options.add_argument('-bet_c', type=app.Parser.FloatSeq(), nargs=3, metavar='', help='Centre-of-gravity (voxels not mm) of initial mesh surface') options.add_argument('-bet_r', type=float, help='Head radius (mm not voxels); initial surface sphere is set to half of this') options.add_argument('-rescale', action='store_true', help='Rescale voxel size provided to BET to 1mm isotropic; can improve results for rodent data') @@ -67,7 +67,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.bet_r is not None: cmd_string += ' -r ' + str(app.ARGS.bet_r) if app.ARGS.bet_c is not None: - cmd_string += ' -c ' + str(app.ARGS.bet_c) + cmd_string += ' -c ' + ' '.join(str(item) for item in app.ARGS.bet_c) # Running BET command run.command(cmd_string) diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index 6f6e3d6dfb..6dc4008372 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -54,7 +54,7 @@ def needs_mean_bzero(): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable if app.ARGS.shells: - run.command('dwiextract input.mif input_shells.mif -shells ' + app.ARGS.shells) + run.command('dwiextract input.mif input_shells.mif -shells ' + ','.join(str(item) for item in app.ARGS.shells)) run.command('dwishellmath input_shells.mif mean shell_traces.mif') else: run.command('dwishellmath input.mif mean shell_traces.mif') diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index c146f57a02..54bb769449 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -85,7 +85,7 @@ def execute(): #pylint: disable=unused-variable # Get lmax information (if provided). sfwm_lmax = [ ] if app.ARGS.lmax: - sfwm_lmax = [ int(x.strip()) for x in app.ARGS.lmax.split(',') ] + sfwm_lmax = app.ARGS.lmax if not len(sfwm_lmax) == len(bvalues): raise MRtrixError('Number of lmax\'s (' + str(len(sfwm_lmax)) + ', as supplied to the -lmax option: ' + ','.join(map(str,sfwm_lmax)) + ') does not match number of unique b-values.') for sfl in sfwm_lmax: diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index fd17765f3f..0309d765c6 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -60,7 +60,7 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Need at least 2 unique b-values (including b=0).') lmax_option = '' if app.ARGS.lmax: - lmax_option = ' -lmax ' + app.ARGS.lmax + lmax_option = ' -lmax ' + ','.join(str(item) for item in app.ARGS.lmax) if not app.ARGS.mask: run.command('maskfilter mask.mif erode mask_eroded.mif -npass ' + str(app.ARGS.erode)) mask_path = 'mask_eroded.mif' diff --git a/lib/mrtrix3/dwi2response/manual.py b/lib/mrtrix3/dwi2response/manual.py index af2ce7a5de..1a8acb473c 100644 --- a/lib/mrtrix3/dwi2response/manual.py +++ b/lib/mrtrix3/dwi2response/manual.py @@ -63,7 +63,7 @@ def execute(): #pylint: disable=unused-variable # Get lmax information (if provided) lmax = [ ] if app.ARGS.lmax: - lmax = [ int(x.strip()) for x in app.ARGS.lmax.split(',') ] + lmax = app.ARGS.lmax if not len(lmax) == len(shells): raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(lmax)) + ') does not match number of b-value shells (' + str(len(shells)) + ')') for shell_l in lmax: diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index 4e92dfcd37..d98cd4d77a 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -90,7 +90,7 @@ def execute(): #pylint: disable=unused-variable # Get lmax information (if provided) wm_lmax = [ ] if app.ARGS.lmax: - wm_lmax = [ int(x.strip()) for x in app.ARGS.lmax.split(',') ] + wm_lmax = app.ARGS.lmax if not len(wm_lmax) == len(shells): raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(wm_lmax)) + ') does not match number of b-values (' + str(len(shells)) + ')') for shell_l in wm_lmax: diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index c7024e219f..eddae00138 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -56,7 +56,7 @@ def supports_mask(): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable lmax_option = '' if app.ARGS.lmax: - lmax_option = ' -lmax ' + app.ARGS.lmax + lmax_option = ' -lmax ' + ','.join(str(item) for item in app.ARGS.lmax) convergence_change = 0.01 * app.ARGS.convergence diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index 0dbe589df6..4f7c0fd8e1 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -57,7 +57,7 @@ def supports_mask(): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable lmax_option = '' if app.ARGS.lmax: - lmax_option = ' -lmax ' + app.ARGS.lmax + lmax_option = ' -lmax ' + ','.join(str(item) for item in app.ARGS.lmax) if app.ARGS.max_iters < 2: raise MRtrixError('Number of iterations must be at least 2') From 6dcfe5353c171a7a86aa2109b175f667aecf1fb2 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Tue, 14 Feb 2023 16:28:52 +1100 Subject: [PATCH 13/75] Added inheritance for TracksIn checks as per (https://github.com/ankitasanil/mrtrix3/pull/1#discussion_r1099607175) Implemented class inheritance to avoid duplicate checks for tractogram input files. Instead, reused the checks from ArgFileIn type via inheritance. --- lib/mrtrix3/app.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index ebd113c61e..22cabad538 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1101,7 +1101,7 @@ def _is_option_group(self, group): not group == self._positionals and \ group.title not in ( 'options', 'optional arguments' ) - class TypeBoolean: + class Boolean: def __call__(self, input_value): processed_value = input_value.lower().strip() if processed_value.lower() == 'true' or processed_value == 'yes': @@ -1163,13 +1163,10 @@ class ImageOut: def __call__(self, input_value): return input_value - class TracksIn: + class TracksIn(ArgFileIn): def __call__(self, input_value): - if not os.path.exists(input_value): - raise argparse.ArgumentTypeError(input_value + ' path does not exist') - elif not os.path.isfile(input_value): - raise argparse.ArgumentTypeError(input_value + ' is not a file') - elif not input_value.endsWith('.tck'): + super().__call__(input_value) + if not input_value.endswith('.tck'): raise argparse.ArgumentTypeError(input_value + ' is not a valid track file') else: return input_value From 935a8aa2821515ad31fe008ef1cfe9bb10fd49ae Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Tue, 7 Mar 2023 16:49:18 +1100 Subject: [PATCH 14/75] Python API: Ability to handle piped commands Changes for handling piped images in the Python API scripts. However, this implementation does not include deletion of the temp/piped images at the end of the command execution. --- core/signal_handler.cpp | 22 +++++++++++++++++++--- lib/mrtrix3/app.py | 5 +++++ lib/mrtrix3/image.py | 4 ++-- lib/mrtrix3/run.py | 2 ++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/core/signal_handler.cpp b/core/signal_handler.cpp index 51f782a361..db3eb75f90 100644 --- a/core/signal_handler.cpp +++ b/core/signal_handler.cpp @@ -135,9 +135,25 @@ namespace MR void mark_file_for_deletion (const std::string& filename) { - while (!flag.test_and_set()); - marked_files.push_back (filename); - flag.clear(); + //ENVVAR name: MRTRIX_DELETE_TMPFILE + //ENVVAR This variable decides whether the temporary piped image + //ENVVAR should be deleted or retained for further processing. + //ENVVAR For example, in case of piped commands from Python API, + //ENVVAR it is necessary to retain the temp files until all + //ENVVAR the piped commands are executed. + char* MRTRIX_DELETE_TMPFILE = getenv("MRTRIX_DELETE_TMPFILE"); + if (MRTRIX_DELETE_TMPFILE != NULL) { + if(strcmp(MRTRIX_DELETE_TMPFILE,"no") != 0) { + while (!flag.test_and_set()); + marked_files.push_back (filename); + flag.clear(); + } + } + else{ + while (!flag.test_and_set()); + marked_files.push_back (filename); + flag.clear(); + } } void unmark_file_for_deletion (const std::string& filename) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 22cabad538..aa25ce24bf 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1157,6 +1157,11 @@ def __call__(self, input_value): class ImageIn: def __call__(self, input_value): + if(input_value == '-'): + if not sys.stdin.isatty(): + input_value = sys.stdin.read().strip() + else: + raise argparse.ArgumentTypeError('Input unavailable in stdin from command before pipe.') return input_value class ImageOut: diff --git a/lib/mrtrix3/image.py b/lib/mrtrix3/image.py index 9f0ecc6d86..83becc5c1f 100644 --- a/lib/mrtrix3/image.py +++ b/lib/mrtrix3/image.py @@ -30,7 +30,7 @@ class Header: def __init__(self, image_path): from mrtrix3 import app, path, run #pylint: disable=import-outside-toplevel filename = path.name_temporary('json') - command = [ run.exe_name(run.version_match('mrinfo')), image_path, '-json_all', filename ] + command = [ run.exe_name(run.version_match('mrinfo')), image_path, '-json_all', filename, '-nodelete' ] if app.VERBOSITY > 1: app.console('Loading header for image file \'' + image_path + '\'') app.debug(str(command)) @@ -146,7 +146,7 @@ def check_3d_nonunity(image_in): #pylint: disable=unused-variable # form is not performed by this function. def mrinfo(image_path, field): #pylint: disable=unused-variable from mrtrix3 import app, run #pylint: disable=import-outside-toplevel - command = [ run.exe_name(run.version_match('mrinfo')), image_path, '-' + field ] + command = [ run.exe_name(run.version_match('mrinfo')), image_path, '-' + field, '-nodelete' ] if app.VERBOSITY > 1: app.console('Command: \'' + ' '.join(command) + '\' (piping data to local storage)') with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=None) as proc: diff --git a/lib/mrtrix3/run.py b/lib/mrtrix3/run.py index e4d22ad853..572f7f2864 100644 --- a/lib/mrtrix3/run.py +++ b/lib/mrtrix3/run.py @@ -79,6 +79,8 @@ def __init__(self): self._scratch_dir = None self.verbosity = 1 + self.env['MRTRIX_DELETE_TMPFILE'] = 'no' + # Acquire a unique index # This ensures that if command() is executed in parallel using different threads, they will # not interfere with one another; but terminate() will also have access to all relevant data From e749fe92354a7e8abb462ee90508f7cab512b033 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Wed, 15 Mar 2023 08:49:46 +1100 Subject: [PATCH 15/75] Changes for output image piping The current implementation is temporary since it doesn't cover all the use-cases. However, it supports a working scenario. --- bin/dwishellmath | 5 +++++ lib/mrtrix3/app.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/bin/dwishellmath b/bin/dwishellmath index 58d55e3e5a..01fa108f23 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -37,6 +37,8 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel from mrtrix3 import app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + import sys + # check inputs and outputs dwi_header = image.Header(path.from_user(app.ARGS.input, False)) if len(dwi_header.size()) != 4: @@ -63,6 +65,9 @@ def execute(): #pylint: disable=unused-variable # make a 4D image with one volume app.warn('Only one unique b-value present in DWI data; command mrmath with -axis 3 option may be preferable') run.command('mrconvert ' + files[0] + ' ' + path.from_user(app.ARGS.output) + ' -axes 0,1,2,-1', mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + + if('mrtrix-tmp-' in app.ARGS.output): + sys.stdout.write(app.ARGS.output) # Execute the script diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index aa25ce24bf..8fa6779c7c 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1166,6 +1166,9 @@ def __call__(self, input_value): class ImageOut: def __call__(self, input_value): + if(input_value == '-'): + result_str = ''.join(random.choice(string.ascii_letters) for i in range(6)) + input_value = 'mrtrix-tmp-' + result_str + '.mif' return input_value class TracksIn(ArgFileIn): From a830bf42a8abd357c64d0390b6e637e252bc43e9 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 12 Jul 2023 21:17:15 +1000 Subject: [PATCH 16/75] Python CLI: Tweaks to custom types Primarily renaming of classes to more closely echo the modifier functions that are used in the C++ usage() function rather than the enumeration that is hidden from most developers. --- bin/dwi2response | 4 +- bin/dwifslpreproc | 8 +-- bin/labelsgmfix | 2 +- bin/mrtrix_cleanup | 4 +- bin/population_template | 42 ++++++------ bin/responsemean | 4 +- lib/mrtrix3/_5ttgen/freesurfer.py | 2 +- lib/mrtrix3/_5ttgen/hsvs.py | 2 +- lib/mrtrix3/app.py | 93 ++++++++++++-------------- lib/mrtrix3/dwi2mask/b02template.py | 2 +- lib/mrtrix3/dwi2mask/fslbet.py | 2 +- lib/mrtrix3/dwi2mask/trace.py | 2 +- lib/mrtrix3/dwi2response/dhollander.py | 2 +- lib/mrtrix3/dwi2response/fa.py | 2 +- lib/mrtrix3/dwi2response/manual.py | 2 +- lib/mrtrix3/dwi2response/msmt_5tt.py | 6 +- lib/mrtrix3/dwi2response/tax.py | 2 +- lib/mrtrix3/dwi2response/tournier.py | 2 +- lib/mrtrix3/dwinormalise/group.py | 6 +- 19 files changed, 92 insertions(+), 97 deletions(-) diff --git a/bin/dwi2response b/bin/dwi2response index 2e17e2b3a8..4429330540 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -35,8 +35,8 @@ def usage(cmdline): #pylint: disable=unused-variable common_options = cmdline.add_argument_group('General dwi2response options') common_options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Provide an initial mask for response voxel selection') common_options.add_argument('-voxels', type=app.Parser.ImageOut(), metavar='image', help='Output an image showing the final voxel selection(s)') - common_options.add_argument('-shells', type=app.Parser.FloatSeq(), help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly)') - common_options.add_argument('-lmax', type=app.Parser.IntSeq(), help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') + common_options.add_argument('-shells', type=app.Parser.SequenceFloat(), help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly)') + common_options.add_argument('-lmax', type=app.Parser.SequenceInt(), help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index 6e50b6deeb..135f756953 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -52,7 +52,7 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Bastiani, M.; Cottaar, M.; Fitzgibbon, S.P.; Suri, S.; Alfaro-Almagro, F.; Sotiropoulos, S.N.; Jbabdi, S.; Andersson, J.L.R. Automated quality control for within and between studies diffusion MRI data using a non-parametric framework for movement and distortion correction. NeuroImage, 2019, 184, 801-812', condition='If using -eddyqc_text or -eddyqc_all option and eddy_quad is installed', is_external=True) cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series to be corrected') cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') - cmdline.add_argument('-json_import', type=app.Parser.ArgFileIn(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') + cmdline.add_argument('-json_import', type=app.Parser.FileIn(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') pe_options.add_argument('-pe_dir', metavar=('PE'), help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') pe_options.add_argument('-readout_time', metavar='time', type=float, help='Manually specify the total readout time of the input series (in seconds)') @@ -66,11 +66,11 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'topup_options' ], False ) eddy_options = cmdline.add_argument_group('Options for affecting the operation of the FSL "eddy" command') eddy_options.add_argument('-eddy_mask', type=app.Parser.ImageIn(), metavar='image', help='Provide a processing mask to use for eddy, instead of having dwifslpreproc generate one internally using dwi2mask') - eddy_options.add_argument('-eddy_slspec', type=app.Parser.ArgFileIn(), metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') + eddy_options.add_argument('-eddy_slspec', type=app.Parser.FileIn(), metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') eddy_options.add_argument('-eddy_options', metavar='" EddyOptions"', help='Manually provide additional command-line options to the eddy command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to eddy)') eddyqc_options = cmdline.add_argument_group('Options for utilising EddyQC') - eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.ArgDirectoryOut(), metavar='directory', help='Copy the various text-based statistical outputs generated by eddy, and the output of eddy_qc (if installed), into an output directory') - eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.ArgDirectoryOut(), metavar='directory', help='Copy ALL outputs generated by eddy (including images), and the output of eddy_qc (if installed), into an output directory') + eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.DirectoryOut(), metavar='directory', help='Copy the various text-based statistical outputs generated by eddy, and the output of eddy_qc (if installed), into an output directory') + eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.DirectoryOut(), metavar='directory', help='Copy ALL outputs generated by eddy (including images), and the output of eddy_qc (if installed), into an output directory') cmdline.flag_mutually_exclusive_options( [ 'eddyqc_text', 'eddyqc_all' ], False ) app.add_dwgrad_export_options(cmdline) app.add_dwgrad_import_options(cmdline) diff --git a/bin/labelsgmfix b/bin/labelsgmfix index 7485ed0e51..82affcbc20 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -38,7 +38,7 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. The effects of SIFT on the reproducibility and biological accuracy of the structural connectome. NeuroImage, 2015, 104, 253-265') cmdline.add_argument('parc', type=app.Parser.ImageIn(), help='The input FreeSurfer parcellation image') cmdline.add_argument('t1', type=app.Parser.ImageIn(), help='The T1 image to be provided to FIRST') - cmdline.add_argument('lut', type=app.Parser.ArgFileIn(), help='The lookup table file that the parcellated image is based on') + cmdline.add_argument('lut', type=app.Parser.FileIn(), help='The lookup table file that the parcellated image is based on') cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output parcellation image') cmdline.add_argument('-premasked', action='store_true', default=False, help='Indicate that brain masking has been applied to the T1 input image') cmdline.add_argument('-sgm_amyg_hipp', action='store_true', default=False, help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures, and also replace their estimates with those from FIRST') diff --git a/bin/mrtrix_cleanup b/bin/mrtrix_cleanup index d82c56d550..feb5d63d09 100755 --- a/bin/mrtrix_cleanup +++ b/bin/mrtrix_cleanup @@ -30,9 +30,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('This script will search the file system at the specified location (and in sub-directories thereof) for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, and attempt to delete them.') cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. Cleanup of such locations should instead be performed explicitly: e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: it may delete temporary items during operation that may lead to unexpected behaviour.') - cmdline.add_argument('path', type=app.Parser.ArgDirectoryIn(), help='Directory from which to commence filesystem search') + cmdline.add_argument('path', type=app.Parser.DirectoryIn(), help='Directory from which to commence filesystem search') cmdline.add_argument('-test', action='store_true', help='Run script in test mode: will list identified files / directories, but not attempt to delete them') - cmdline.add_argument('-failed', type=app.Parser.ArgFileOut(), metavar='file', help='Write list of items that the script failed to delete to a text file') + cmdline.add_argument('-failed', type=app.Parser.FileOut(), metavar='file', help='Write list of items that the script failed to delete to a text file') cmdline.flag_mutually_exclusive_options([ 'test', 'failed' ]) diff --git a/bin/population_template b/bin/population_template index c6662b016a..42aee18761 100755 --- a/bin/population_template +++ b/bin/population_template @@ -50,47 +50,47 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_synopsis('Generates an unbiased group-average template from a series of images') cmdline.add_description('First a template is optimised with linear registration (rigid and/or affine, both by default), then non-linear registration is used to optimise the template further.') - cmdline.add_argument("input_dir", nargs='+', type=app.Parser.ArgDirectoryIn(), help='Input directory containing all images used to build the template') - cmdline.add_argument("template", type=app.Parser.ImageOut(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') + cmdline.add_argument('input_dir', nargs='+', help='Input directory containing all images used to build the template') + cmdline.add_argument('template', type=app.Parser.ImageOut(), help='Corresponding output template image. For multi-contrast registration, provide multiple paired input_dir and template arguments. Example: WM_dir WM_template.mif GM_dir GM_template.mif') options = cmdline.add_argument_group('Multi-contrast options') - options.add_argument('-mc_weight_initial_alignment', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') - options.add_argument('-mc_weight_rigid', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_affine', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_nl', type=app.Parser().FloatSeq(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_initial_alignment', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') + options.add_argument('-mc_weight_rigid', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_affine', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_nl', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') linoptions = cmdline.add_argument_group('Options for the linear registration') linoptions.add_argument('-linear_no_pause', action='store_true', help='Do not pause the script if a linear registration seems implausible') linoptions.add_argument('-linear_no_drift_correction', action='store_true', help='Deactivate correction of template appearance (scale and shear) over iterations') linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), none (no robust estimator). Default: none.') - linoptions.add_argument('-rigid_scale', type=app.Parser().FloatSeq(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) - linoptions.add_argument('-rigid_lmax', type=app.Parser().IntSeq(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) - linoptions.add_argument('-rigid_niter', type=app.Parser().IntSeq(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') - linoptions.add_argument('-affine_scale', type=app.Parser().FloatSeq(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) - linoptions.add_argument('-affine_lmax', type=app.Parser().IntSeq(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) - linoptions.add_argument('-affine_niter', type=app.Parser().IntSeq(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-rigid_scale', type=app.Parser().SequenceFloat(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) + linoptions.add_argument('-rigid_lmax', type=app.Parser().SequenceInt(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) + linoptions.add_argument('-rigid_niter', type=app.Parser().SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-affine_scale', type=app.Parser().SequenceFloat(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) + linoptions.add_argument('-affine_lmax', type=app.Parser().SequenceInt(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) + linoptions.add_argument('-affine_niter', type=app.Parser().SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') nloptions = cmdline.add_argument_group('Options for the non-linear registration') - nloptions.add_argument('-nl_scale', type=app.Parser().FloatSeq(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) - nloptions.add_argument('-nl_lmax', type=app.Parser().IntSeq(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) - nloptions.add_argument('-nl_niter', type=app.Parser().IntSeq(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) + nloptions.add_argument('-nl_scale', type=app.Parser().SequenceFloat(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) + nloptions.add_argument('-nl_lmax', type=app.Parser().SequenceInt(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) + nloptions.add_argument('-nl_niter', type=app.Parser().SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) nloptions.add_argument('-nl_update_smooth', type=float, default=DEFAULT_NL_UPDATE_SMOOTH, help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_UPDATE_SMOOTH) + ' x voxel_size)') nloptions.add_argument('-nl_disp_smooth', type=float, default=DEFAULT_NL_DISP_SMOOTH, help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_DISP_SMOOTH) + ' x voxel_size)') nloptions.add_argument('-nl_grad_step', type=float, default=DEFAULT_NL_GRAD_STEP, help='The gradient step size for non-linear registration (Default: ' + str(DEFAULT_NL_GRAD_STEP) + ')') options = cmdline.add_argument_group('Input, output and general options') options.add_argument('-type', choices=REGISTRATION_MODES, help='Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"' + x + '"' for x in REGISTRATION_MODES if "_" in x), default='rigid_affine_nonlinear') - options.add_argument('-voxel_size', type=app.Parser().FloatSeq(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') + options.add_argument('-voxel_size', type=app.Parser().SequenceFloat(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') options.add_argument('-initial_alignment', choices=INITIAL_ALIGNMENT, default='mass', help='Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none".') - options.add_argument('-mask_dir', type=app.Parser().ArgDirectoryIn(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') - options.add_argument('-warp_dir', type=app.Parser().ArgDirectoryOut(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') - options.add_argument('-transformed_dir', type=app.Parser().ArgDirectoryOut(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') - options.add_argument('-linear_transformations_dir', type=app.Parser().ArgDirectoryOut(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') + options.add_argument('-mask_dir', type=app.Parser().DirectoryIn(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') + options.add_argument('-warp_dir', type=app.Parser().DirectoryOut(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') + options.add_argument('-transformed_dir', type=app.Parser().DirectoryOut(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') + options.add_argument('-linear_transformations_dir', type=app.Parser().DirectoryOut(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') options.add_argument('-template_mask', type=app.Parser().ImageOut(), help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') options.add_argument('-noreorientation', action='store_true', help='Turn off FOD reorientation in mrregister. Reorientation is on by default if the number of volumes in the 4th dimension corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series (i.e. 6, 15, 28, 45, 66 etc)') options.add_argument('-leave_one_out', choices=LEAVE_ONE_OUT, default='auto', help='Register each input image to a template that does not contain that image. Valid choices: ' + ', '.join(LEAVE_ONE_OUT) + '. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) ') options.add_argument('-aggregate', choices=AGGREGATION_MODES, help='Measure used to aggregate information from transformed images to the template image. Valid choices: %s. Default: mean' % ', '.join(AGGREGATION_MODES)) - options.add_argument('-aggregation_weights', type=app.Parser().ArgFileIn(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') + options.add_argument('-aggregation_weights', type=app.Parser().FileIn(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') options.add_argument('-nanmask', action='store_true', help='Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. Only works if -mask_dir has been input.') options.add_argument('-copy_input', action='store_true', help='Copy input images and masks into local scratch directory.') options.add_argument('-delete_temporary_files', action='store_true', help='Delete temporary files from scratch directory during template creation.') diff --git a/bin/responsemean b/bin/responsemean index f4c0e3b153..f0d59f1f8c 100755 --- a/bin/responsemean +++ b/bin/responsemean @@ -27,8 +27,8 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('Example usage: ' + os.path.basename(sys.argv[0]) + ' input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt') cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), as well as the same number of coefficients per line.') cmdline.add_description('As long as the number of unique b-values is identical across all input files, the coefficients will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the ' + os.path.basename(sys.argv[0]) + ' command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied.') - cmdline.add_argument('inputs', type=app.Parser.ArgFileIn(), help='The input response function files', nargs='+') - cmdline.add_argument('output', type=app.Parser.ArgFileOut(), help='The output mean response function file') + cmdline.add_argument('inputs', type=app.Parser.FileIn(), help='The input response function files', nargs='+') + cmdline.add_argument('output', type=app.Parser.FileOut(), help='The output mean response function file') cmdline.add_argument('-legacy', action='store_true', help='Use the legacy behaviour of former command \'average_response\': average response function coefficients directly, without compensating for global magnitude differences between input files') diff --git a/lib/mrtrix3/_5ttgen/freesurfer.py b/lib/mrtrix3/_5ttgen/freesurfer.py index 995eb433ff..2577693d32 100644 --- a/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/lib/mrtrix3/_5ttgen/freesurfer.py @@ -26,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input FreeSurfer parcellation image (any image containing \'aseg\' in its name)') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') options = parser.add_argument_group('Options specific to the \'freesurfer\' algorithm') - options.add_argument('-lut', type=app.Parser.ArgFileIn(), help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') + options.add_argument('-lut', type=app.Parser.FileIn(), help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') diff --git a/lib/mrtrix3/_5ttgen/hsvs.py b/lib/mrtrix3/_5ttgen/hsvs.py index 9fe95b1fa0..c25e71c067 100644 --- a/lib/mrtrix3/_5ttgen/hsvs.py +++ b/lib/mrtrix3/_5ttgen/hsvs.py @@ -34,7 +34,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('hsvs', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate a 5TT image based on Hybrid Surface and Volume Segmentation (HSVS), using FreeSurfer and FSL tools') - parser.add_argument('input', type=app.Parser.ArgDirectoryIn(), help='The input FreeSurfer subject directory') + parser.add_argument('input', type=app.Parser.DirectoryIn(), help='The input FreeSurfer subject directory') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') parser.add_argument('-template', type=app.Parser.ImageIn(), help='Provide an image that will form the template for the generated 5TT image') parser.add_argument('-hippocampi', choices=HIPPOCAMPI_CHOICES, help='Select method to be used for hippocampi (& amygdalae) segmentation; options are: ' + ','.join(HIPPOCAMPI_CHOICES)) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 8fa6779c7c..dddba926d9 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -122,7 +122,7 @@ def _execute(module): #pylint: disable=unused-variable module.usage(CMDLINE) except AttributeError: CMDLINE = None - raise + raise ######################################################################################################################## # Note that everything after this point will only be executed if the script is designed to operate against the library # @@ -1101,90 +1101,85 @@ def _is_option_group(self, group): not group == self._positionals and \ group.title not in ( 'options', 'optional arguments' ) - class Boolean: + # Various callable types for use as argparse argument types + class Bool: def __call__(self, input_value): - processed_value = input_value.lower().strip() - if processed_value.lower() == 'true' or processed_value == 'yes': + processed_value = input_value.strip().lower() + if processed_value in ['true', 'yes']: return True - elif processed_value.lower() == 'false' or processed_value == 'no': + if processed_value in ['false', 'no']: return False - else: - raise argparse.ArgumentTypeError('Entered value is not of type boolean') + try: + processed_value = int(processed_value) + except ValueError: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as boolean value"') + return bool(processed_value) - class IntSeq: + class SequenceInt: def __call__(self, input_value): - int_list = [] try: - int_list = [int(i) for i in input_value.split(',')] - except (ValueError, NameError) as e: - raise argparse.ArgumentTypeError('Entered value is not an integer sequence') - return int_list - - class FloatSeq: + return [int(i) for i in input_value.split(',')] + except ValueError: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer sequence') + + class SequenceFloat: def __call__(self, input_value): - float_list = [] try: - float_list = [float(i) for i in input_value.split(',')] - except (ValueError, NameError) as e: - raise argparse.ArgumentTypeError('Entered value is not a float sequence') - return float_list + return [float(i) for i in input_value.split(',')] + except ValueError: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point sequence') - class ArgDirectoryIn: + class DirectoryIn: def __call__(self, input_value): if not os.path.exists(input_value): - raise argparse.ArgumentTypeError(input_value + ' does not exist') - elif not os.path.isdir(input_value): - raise argparse.ArgumentTypeError(input_value + ' is not a directory') - else: - return input_value + raise argparse.ArgumentTypeError('Input directory "' + input_value + '" does not exist') + if not os.path.isdir(input_value): + raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a directory') + return input_value - class ArgDirectoryOut: + class DirectoryOut: def __call__(self, input_value): return input_value - class ArgFileIn: + class FileIn: def __call__(self, input_value): if not os.path.exists(input_value): - raise argparse.ArgumentTypeError(input_value + ' path does not exist') - elif not os.path.isfile(input_value): - raise argparse.ArgumentTypeError(input_value + ' is not a file') - else: - return input_value + raise argparse.ArgumentTypeError('Input file "' + input_value + '" does not exist') + if not os.path.isfile(input_value): + raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a file') + return input_value - class ArgFileOut: + class FileOut: def __call__(self, input_value): return input_value class ImageIn: def __call__(self, input_value): - if(input_value == '-'): - if not sys.stdin.isatty(): - input_value = sys.stdin.read().strip() - else: - raise argparse.ArgumentTypeError('Input unavailable in stdin from command before pipe.') + if input_value == '-': + if sys.stdin.isatty(): + raise argparse.ArgumentTypeError('Input piped image unavailable from stdin') + input_value = sys.stdin.read().strip() return input_value class ImageOut: def __call__(self, input_value): - if(input_value == '-'): - result_str = ''.join(random.choice(string.ascii_letters) for i in range(6)) + if input_value == '-': + result_str = ''.join(random.choice(string.ascii_letters) for _ in range(6)) input_value = 'mrtrix-tmp-' + result_str + '.mif' return input_value - class TracksIn(ArgFileIn): + class TracksIn(FileIn): def __call__(self, input_value): super().__call__(input_value) if not input_value.endswith('.tck'): - raise argparse.ArgumentTypeError(input_value + ' is not a valid track file') - else: - return input_value + raise argparse.ArgumentTypeError('Input tractogram file "' + input_value + '" is not a valid track file') + return input_value class TracksOut: def __call__(self, input_value): - if not input_value.endsWith('.tck'): - raise argparse.ArgumentTypeError(input_value + ' must use the .tck suffix') - else: - return input_value + if not input_value.endswith('.tck'): + raise argparse.ArgumentTypeError('Output tractogram path "' + input_value + '" does not use the requisite ".tck" suffix') + return input_value diff --git a/lib/mrtrix3/dwi2mask/b02template.py b/lib/mrtrix3/dwi2mask/b02template.py index bd02e416e2..1b9420fe25 100644 --- a/lib/mrtrix3/dwi2mask/b02template.py +++ b/lib/mrtrix3/dwi2mask/b02template.py @@ -74,7 +74,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable ants_options.add_argument('-ants_options', help='Provide options to be passed to the ANTs registration command (see Description)') fsl_options = parser.add_argument_group('Options applicable when using the FSL software for registration') fsl_options.add_argument('-flirt_options', metavar='" FlirtOptions"', help='Command-line options to pass to the FSL flirt command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to flirt)') - fsl_options.add_argument('-fnirt_config', type=app.Parser.ArgFileIn(), metavar='file', help='Specify a FNIRT configuration file for registration') + fsl_options.add_argument('-fnirt_config', type=app.Parser.FileIn(), metavar='file', help='Specify a FNIRT configuration file for registration') diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index 7c6f2da72a..ec34c045d7 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -29,7 +29,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the \'fslbet\' algorithm') options.add_argument('-bet_f', type=float, help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') options.add_argument('-bet_g', type=float, help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') - options.add_argument('-bet_c', type=app.Parser.FloatSeq(), nargs=3, metavar='', help='Centre-of-gravity (voxels not mm) of initial mesh surface') + options.add_argument('-bet_c', type=app.Parser.SequenceFloat(), nargs=3, metavar='', help='Centre-of-gravity (voxels not mm) of initial mesh surface') options.add_argument('-bet_r', type=float, help='Head radius (mm not voxels); initial surface sphere is set to half of this') options.add_argument('-rescale', action='store_true', help='Rescale voxel size provided to BET to 1mm isotropic; can improve results for rodent data') diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index 6dc4008372..7feb0fb0cb 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -26,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'trace\' algorithm') - options.add_argument('-shells', type=app.Parser.FloatSeq(), help='Comma-separated list of shells used to generate trace-weighted images for masking') + options.add_argument('-shells', type=app.Parser.SequenceFloat(), help='Comma-separated list of shells used to generate trace-weighted images for masking') options.add_argument('-clean_scale', type=int, default=DEFAULT_CLEAN_SCALE, diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index 54bb769449..46e1db08dc 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -32,7 +32,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Mito, R.; Raffelt, D. & Connelly, A. Improved white matter response function estimation for 3-tissue constrained spherical deconvolution. Proc Intl Soc Mag Reson Med, 2019, 555', condition='If -wm_algo option is not used') parser.add_argument('input', type=app.Parser.ImageIn(), help='Input DWI dataset') - parser.add_argument('out_sfwm', type=app.Parser.ArgFileOut(), help='Output single-fibre WM response function text file') + parser.add_argument('out_sfwm', type=app.Parser.FileOut(), help='Output single-fibre WM response function text file') parser.add_argument('out_gm', type=app.Parser.ImageOut(), help='Output GM response function text file') parser.add_argument('out_csf', type=app.Parser.ImageOut(), help='Output CSF response function text file') options = parser.add_argument_group('Options for the \'dhollander\' algorithm') diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index 0309d765c6..c6dfb86f44 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -25,7 +25,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Use the old FA-threshold heuristic for single-fibre voxel selection and response function estimation') parser.add_citation('Tournier, J.-D.; Calamante, F.; Gadian, D. G. & Connelly, A. Direct estimation of the fiber orientation density function from diffusion-weighted MRI data using spherical deconvolution. NeuroImage, 2004, 23, 1176-1185') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('output', type=app.Parser.ArgFileOut(), help='The output response function text file') + parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'fa\' algorithm') options.add_argument('-erode', type=int, default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') options.add_argument('-number', type=int, default=300, help='The number of highest-FA voxels to use') diff --git a/lib/mrtrix3/dwi2response/manual.py b/lib/mrtrix3/dwi2response/manual.py index 1a8acb473c..a2d27271d0 100644 --- a/lib/mrtrix3/dwi2response/manual.py +++ b/lib/mrtrix3/dwi2response/manual.py @@ -25,7 +25,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Derive a response function using an input mask image alone (i.e. pre-selected voxels)') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('in_voxels', type=app.Parser.ImageIn(), help='Input voxel selection mask') - parser.add_argument('output', type=app.Parser.ArgFileOut(), help='Output response function text file') + parser.add_argument('output', type=app.Parser.FileOut(), help='Output response function text file') options = parser.add_argument_group('Options specific to the \'manual\' algorithm') options.add_argument('-dirs', type=app.Parser.ImageIn(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index d98cd4d77a..cfa80fe4da 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -30,9 +30,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Jeurissen, B.; Tournier, J.-D.; Dhollander, T.; Connelly, A. & Sijbers, J. Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. NeuroImage, 2014, 103, 411-426') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('in_5tt', type=app.Parser.ImageIn(), help='Input co-registered 5TT image') - parser.add_argument('out_wm', type=app.Parser.ArgFileOut(), help='Output WM response text file') - parser.add_argument('out_gm', type=app.Parser.ArgFileOut(), help='Output GM response text file') - parser.add_argument('out_csf', type=app.Parser.ArgFileOut(), help='Output CSF response text file') + parser.add_argument('out_wm', type=app.Parser.FileOut(), help='Output WM response text file') + parser.add_argument('out_gm', type=app.Parser.FileOut(), help='Output GM response text file') + parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response text file') options = parser.add_argument_group('Options specific to the \'msmt_5tt\' algorithm') options.add_argument('-dirs', type=app.Parser.ImageIn(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') options.add_argument('-fa', type=float, default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index eddae00138..5370dbfb80 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -25,7 +25,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Use the Tax et al. (2014) recursive calibration algorithm for single-fibre voxel selection and response function estimation') parser.add_citation('Tax, C. M.; Jeurissen, B.; Vos, S. B.; Viergever, M. A. & Leemans, A. Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. NeuroImage, 2014, 86, 67-80') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('output', type=app.Parser.ArgFileOut(), help='The output response function text file') + parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tax\' algorithm') options.add_argument('-peak_ratio', type=float, default=0.1, help='Second-to-first-peak amplitude ratio threshold') options.add_argument('-max_iters', type=int, default=20, help='Maximum number of iterations') diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index 4f7c0fd8e1..8e32cea551 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -25,7 +25,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Use the Tournier et al. (2013) iterative algorithm for single-fibre voxel selection and response function estimation') parser.add_citation('Tournier, J.-D.; Calamante, F. & Connelly, A. Determination of the appropriate b-value and number of gradient directions for high-angular-resolution diffusion-weighted imaging. NMR Biomedicine, 2013, 26, 1775-1786') parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('output', type=app.Parser.ArgFileOut(), help='The output response function text file') + parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tournier\' algorithm') options.add_argument('-number', type=int, default=300, help='Number of single-fibre voxels to use when calculating response function') options.add_argument('-iter_voxels', type=int, default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') diff --git a/lib/mrtrix3/dwinormalise/group.py b/lib/mrtrix3/dwinormalise/group.py index 7e6f1e8e63..c8263a8bfd 100644 --- a/lib/mrtrix3/dwinormalise/group.py +++ b/lib/mrtrix3/dwinormalise/group.py @@ -25,9 +25,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Performs a global DWI intensity normalisation on a group of subjects using the median b=0 white matter value as the reference') parser.add_description('The white matter mask is estimated from a population average FA template then warped back to each subject to perform the intensity normalisation. Note that bias field correction should be performed prior to this step.') parser.add_description('All input DWI files must contain an embedded diffusion gradient table; for this reason, these images must all be in either .mif or .mif.gz format.') - parser.add_argument('input_dir', type=app.Parser.ArgDirectoryIn(), help='The input directory containing all DWI images') - parser.add_argument('mask_dir', type=app.Parser.ArgDirectoryIn(), help='Input directory containing brain masks, corresponding to one per input image (with the same file name prefix)') - parser.add_argument('output_dir', type=app.Parser.ArgDirectoryOut(), help='The output directory containing all of the intensity normalised DWI images') + parser.add_argument('input_dir', type=app.Parser.DirectoryIn(), help='The input directory containing all DWI images') + parser.add_argument('mask_dir', type=app.Parser.DirectoryIn(), help='Input directory containing brain masks, corresponding to one per input image (with the same file name prefix)') + parser.add_argument('output_dir', type=app.Parser.DirectoryOut(), help='The output directory containing all of the intensity normalised DWI images') parser.add_argument('fa_template', type=app.Parser.ImageOut(), help='The output population-specific FA template, which is thresholded to estimate a white matter mask') parser.add_argument('wm_mask', type=app.Parser.ImageOut(), help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') parser.add_argument('-fa_threshold', default='0.4', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4)') From 45608808311c0dbd815fc09d9515170005a2787d Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 13 Jul 2023 21:22:02 +1000 Subject: [PATCH 17/75] Python CLI: Code cleanup Addressing multiple comments in PR #2678. --- bin/dwi2response | 20 ++----- bin/dwifslpreproc | 4 +- bin/dwishellmath | 7 +-- core/signal_handler.cpp | 20 +++---- docs/reference/commands/5ttgen.rst | 8 +-- docs/reference/commands/dwi2mask.rst | 14 ++--- docs/reference/commands/dwi2response.rst | 60 +++++++++---------- docs/reference/commands/dwibiascorrect.rst | 12 ++-- docs/reference/commands/dwinormalise.rst | 2 +- docs/reference/commands/mrregister.rst | 2 +- docs/reference/commands/mrtrix_cleanup.rst | 2 +- .../commands/population_template.rst | 8 +-- docs/reference/commands/responsemean.rst | 4 +- docs/reference/environment_variables.rst | 9 +++ lib/mrtrix3/_5ttgen/freesurfer.py | 2 +- lib/mrtrix3/_5ttgen/fsl.py | 2 +- lib/mrtrix3/_5ttgen/hsvs.py | 2 +- lib/mrtrix3/dwi2mask/b02template.py | 4 +- lib/mrtrix3/dwi2mask/consensus.py | 2 +- lib/mrtrix3/dwi2mask/fslbet.py | 2 +- lib/mrtrix3/dwi2mask/mean.py | 4 +- lib/mrtrix3/dwi2mask/trace.py | 2 +- lib/mrtrix3/dwi2response/dhollander.py | 18 +++--- lib/mrtrix3/dwi2response/manual.py | 24 ++++---- lib/mrtrix3/dwi2response/msmt_5tt.py | 22 +++---- lib/mrtrix3/run.py | 3 +- 26 files changed, 121 insertions(+), 138 deletions(-) diff --git a/bin/dwi2response b/bin/dwi2response index 4429330540..71ad5ec82d 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -35,8 +35,8 @@ def usage(cmdline): #pylint: disable=unused-variable common_options = cmdline.add_argument_group('General dwi2response options') common_options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Provide an initial mask for response voxel selection') common_options.add_argument('-voxels', type=app.Parser.ImageOut(), metavar='image', help='Output an image showing the final voxel selection(s)') - common_options.add_argument('-shells', type=app.Parser.SequenceFloat(), help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly)') - common_options.add_argument('-lmax', type=app.Parser.SequenceInt(), help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') + common_options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired)') + common_options.add_argument('-lmax', type=app.Parser.SequenceInt(), metavar='values', help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory @@ -61,21 +61,13 @@ def execute(): #pylint: disable=unused-variable # Sanitise some inputs, and get ready for data import if app.ARGS.lmax: - try: - lmax = app.ARGS.lmax - if any(lmax_value%2 for lmax_value in lmax): - raise MRtrixError('Value of lmax must be even') - except ValueError as exception: - raise MRtrixError('Parameter lmax must be a number') from exception - if alg.needs_single_shell() and not len(lmax) == 1: + if any(lmax%2 for lmax in app.ARGS.lmax): + raise MRtrixError('Value(s) of lmax must be even') + if alg.needs_single_shell() and not len(app.ARGS.lmax) == 1: raise MRtrixError('Can only specify a single lmax value for single-shell algorithms') shells_option = '' if app.ARGS.shells: - try: - shells_values = [ int(round(x)) for x in app.ARGS.shells ] - except ValueError as exception: - raise MRtrixError('-shells option should provide a comma-separated list of b-values') from exception - if alg.needs_single_shell() and not len(shells_values) == 1: + if alg.needs_single_shell() and len(app.ARGS.shells) != 1: raise MRtrixError('Can only specify a single b-value shell for single-shell algorithms') shells_option = ' -shells ' + ','.join(str(item) for item in app.ARGS.shells) singleshell_option = '' diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index 135f756953..41770abc8f 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -52,9 +52,9 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Bastiani, M.; Cottaar, M.; Fitzgibbon, S.P.; Suri, S.; Alfaro-Almagro, F.; Sotiropoulos, S.N.; Jbabdi, S.; Andersson, J.L.R. Automated quality control for within and between studies diffusion MRI data using a non-parametric framework for movement and distortion correction. NeuroImage, 2019, 184, 801-812', condition='If using -eddyqc_text or -eddyqc_all option and eddy_quad is installed', is_external=True) cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series to be corrected') cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') - cmdline.add_argument('-json_import', type=app.Parser.FileIn(), metavar=('file'), help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') + cmdline.add_argument('-json_import', type=app.Parser.FileIn(), metavar='file', help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') - pe_options.add_argument('-pe_dir', metavar=('PE'), help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') + pe_options.add_argument('-pe_dir', metavar='PE', help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') pe_options.add_argument('-readout_time', metavar='time', type=float, help='Manually specify the total readout time of the input series (in seconds)') distcorr_options = cmdline.add_argument_group('Options for achieving correction of susceptibility distortions') distcorr_options.add_argument('-se_epi', type=app.Parser.ImageIn(), metavar='image', help='Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series)') diff --git a/bin/dwishellmath b/bin/dwishellmath index 01fa108f23..a59227595c 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -37,8 +37,7 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel from mrtrix3 import app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel - import sys - + # check inputs and outputs dwi_header = image.Header(path.from_user(app.ARGS.input, False)) if len(dwi_header.size()) != 4: @@ -65,9 +64,7 @@ def execute(): #pylint: disable=unused-variable # make a 4D image with one volume app.warn('Only one unique b-value present in DWI data; command mrmath with -axis 3 option may be preferable') run.command('mrconvert ' + files[0] + ' ' + path.from_user(app.ARGS.output) + ' -axes 0,1,2,-1', mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) - - if('mrtrix-tmp-' in app.ARGS.output): - sys.stdout.write(app.ARGS.output) + # Execute the script diff --git a/core/signal_handler.cpp b/core/signal_handler.cpp index db3eb75f90..0b48a18ced 100644 --- a/core/signal_handler.cpp +++ b/core/signal_handler.cpp @@ -135,25 +135,19 @@ namespace MR void mark_file_for_deletion (const std::string& filename) { - //ENVVAR name: MRTRIX_DELETE_TMPFILE + //ENVVAR name: MRTRIX_PRESERVE_TMPFILE //ENVVAR This variable decides whether the temporary piped image - //ENVVAR should be deleted or retained for further processing. - //ENVVAR For example, in case of piped commands from Python API, + //ENVVAR should be preserved rather than the usual behaviour of + //ENVVAR deletion at command completion. + //ENVVAR For example, in case of piped commands from Python API, //ENVVAR it is necessary to retain the temp files until all //ENVVAR the piped commands are executed. - char* MRTRIX_DELETE_TMPFILE = getenv("MRTRIX_DELETE_TMPFILE"); - if (MRTRIX_DELETE_TMPFILE != NULL) { - if(strcmp(MRTRIX_DELETE_TMPFILE,"no") != 0) { - while (!flag.test_and_set()); - marked_files.push_back (filename); - flag.clear(); - } - } - else{ + const char* const MRTRIX_PRESERVE_TMPFILE = getenv("MRTRIX_PRESERVE_TMPFILE"); + if (!MRTRIX_PRESERVE_TMPFILE || !to(MRTRIX_PRESERVE_TMPFILE)) { while (!flag.test_and_set()); marked_files.push_back (filename); flag.clear(); - } + } } void unmark_file_for_deletion (const std::string& filename) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index 7b124209c6..d24ebeb71d 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -116,7 +116,7 @@ Options Options specific to the 'freesurfer' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-lut** Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt) +- **-lut file** Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt) Options common to all 5ttgen algorithms ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -207,9 +207,9 @@ Options Options specific to the 'fsl' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-t2 ** Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST +- **-t2 image** Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST -- **-mask** Manually provide a brain mask, rather than deriving one in the script +- **-mask image** Manually provide a brain mask, rather than deriving one in the script - **-premasked** Indicate that brain masking has already been applied to the input image @@ -393,7 +393,7 @@ Usage Options ------- -- **-template** Provide an image that will form the template for the generated 5TT image +- **-template image** Provide an image that will form the template for the generated 5TT image - **-hippocampi** Select method to be used for hippocampi (& amygdalae) segmentation; options are: subfields,first,aseg diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index e05e591318..e9a35ce233 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -314,7 +314,7 @@ Usage Description ----------- -This script currently assumes that the template image provided via the -template option is T2-weighted, and can therefore be trivially registered to a mean b=0 image. +This script currently assumes that the template image provided via the first input to the -template option is T2-weighted, and can therefore be trivially registered to a mean b=0 image. Command-line option -ants_options can be used with either the "antsquick" or "antsfull" software options. In both cases, image dimensionality is assumed to be 3, and so this should be omitted from the user-specified options.The input can be either a string (encased in double-quotes if more than one option is specified), or a path to a text file containing the requested options. In the case of the "antsfull" software option, one will require the names of the fixed and moving images that are provided to the antsRegistration command: these are "template_image.nii" and "bzero.nii" respectively. @@ -326,12 +326,12 @@ Options applicable when using the FSL software for registration - **-flirt_options " FlirtOptions"** Command-line options to pass to the FSL flirt command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to flirt) -- **-fnirt_config FILE** Specify a FNIRT configuration file for registration +- **-fnirt_config file** Specify a FNIRT configuration file for registration Options applicable when using the ANTs software for registration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-ants_options** Provide options to be passed to the ANTs registration command (see Description) +- **-ants_options " ANTsOptions"** Provide options to be passed to the ANTs registration command (see Description) Options specific to the "template" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -433,7 +433,7 @@ Options specific to the "consensus" algorithm - **-algorithms** Provide a list of dwi2mask algorithms that are to be utilised -- **-masks** Export a 4D image containing the individual algorithm masks +- **-masks image** Export a 4D image containing the individual algorithm masks - **-template TemplateImage MaskImage** Provide a template image and corresponding mask for those algorithms requiring such @@ -530,7 +530,7 @@ Options specific to the 'fslbet' algorithm - **-bet_g** Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top -- **-bet_c ** Centre-of-gravity (voxels not mm) of initial mesh surface +- **-bet_c i,j,k** Centre-of-gravity (voxels not mm) of initial mesh surface - **-bet_r** Head radius (mm not voxels); initial surface sphere is set to half of this @@ -797,7 +797,7 @@ Options Options specific to the 'mean' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-shells** Comma separated list of shells to be included in the volume averaging +- **-shells bvalues** Comma separated list of shells to be included in the volume averaging - **-clean_scale** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) @@ -895,7 +895,7 @@ Options for turning 'dwi2mask trace' into an iterative algorithm Options specific to the 'trace' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-shells** Comma separated list of shells used to generate trace-weighted images for masking +- **-shells bvalues** Comma-separated list of shells used to generate trace-weighted images for masking - **-clean_scale** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index be87379787..1d69c65c84 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -42,13 +42,13 @@ Options for importing the diffusion gradient table General dwi2response options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask** Provide an initial mask for response voxel selection +- **-mask image** Provide an initial mask for response voxel selection -- **-voxels** Output an image showing the final voxel selection(s) +- **-voxels image** Output an image showing the final voxel selection(s) -- **-shells** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly) +- **-shells bvalues** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired) -- **-lmax** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) +- **-lmax values** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -159,13 +159,13 @@ Options for importing the diffusion gradient table General dwi2response options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask** Provide an initial mask for response voxel selection +- **-mask image** Provide an initial mask for response voxel selection -- **-voxels** Output an image showing the final voxel selection(s) +- **-voxels image** Output an image showing the final voxel selection(s) -- **-shells** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly) +- **-shells bvalues** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired) -- **-lmax** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) +- **-lmax values** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -267,13 +267,13 @@ Options for importing the diffusion gradient table General dwi2response options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask** Provide an initial mask for response voxel selection +- **-mask image** Provide an initial mask for response voxel selection -- **-voxels** Output an image showing the final voxel selection(s) +- **-voxels image** Output an image showing the final voxel selection(s) -- **-shells** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly) +- **-shells bvalues** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired) -- **-lmax** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) +- **-lmax values** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -358,7 +358,7 @@ Options Options specific to the 'manual' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-dirs** Manually provide the fibre direction in each voxel (a tensor fit will be used otherwise) +- **-dirs image** Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise) Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -370,13 +370,13 @@ Options for importing the diffusion gradient table General dwi2response options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask** Provide an initial mask for response voxel selection +- **-mask image** Provide an initial mask for response voxel selection -- **-voxels** Output an image showing the final voxel selection(s) +- **-voxels image** Output an image showing the final voxel selection(s) -- **-shells** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly) +- **-shells bvalues** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired) -- **-lmax** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) +- **-lmax values** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -461,7 +461,7 @@ Options Options specific to the 'msmt_5tt' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-dirs** Manually provide the fibre direction in each voxel (a tensor fit will be used otherwise) +- **-dirs image** Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise) - **-fa** Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2) @@ -481,13 +481,13 @@ Options for importing the diffusion gradient table General dwi2response options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask** Provide an initial mask for response voxel selection +- **-mask image** Provide an initial mask for response voxel selection -- **-voxels** Output an image showing the final voxel selection(s) +- **-voxels image** Output an image showing the final voxel selection(s) -- **-shells** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly) +- **-shells bvalues** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired) -- **-lmax** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) +- **-lmax values** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -587,13 +587,13 @@ Options for importing the diffusion gradient table General dwi2response options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask** Provide an initial mask for response voxel selection +- **-mask image** Provide an initial mask for response voxel selection -- **-voxels** Output an image showing the final voxel selection(s) +- **-voxels image** Output an image showing the final voxel selection(s) -- **-shells** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly) +- **-shells bvalues** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired) -- **-lmax** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) +- **-lmax values** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -695,13 +695,13 @@ Options for importing the diffusion gradient table General dwi2response options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask** Provide an initial mask for response voxel selection +- **-mask image** Provide an initial mask for response voxel selection -- **-voxels** Output an image showing the final voxel selection(s) +- **-voxels image** Output an image showing the final voxel selection(s) -- **-shells** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values, b=0 must be included explicitly) +- **-shells bvalues** The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired) -- **-lmax** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) +- **-lmax values** The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwibiascorrect.rst b/docs/reference/commands/dwibiascorrect.rst index 1feee0e948..b9e6b3c231 100644 --- a/docs/reference/commands/dwibiascorrect.rst +++ b/docs/reference/commands/dwibiascorrect.rst @@ -36,9 +36,9 @@ Options for importing the diffusion gradient table Options common to all dwibiascorrect algorithms ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask image** Manually provide a mask image for bias field estimation +- **-mask image** Manually provide an input mask image for bias field estimation -- **-bias image** Output the estimated bias field +- **-bias image** Output an image containing the estimated bias field Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -136,9 +136,9 @@ Options for importing the diffusion gradient table Options common to all dwibiascorrect algorithms ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask image** Manually provide a mask image for bias field estimation +- **-mask image** Manually provide an input mask image for bias field estimation -- **-bias image** Output the estimated bias field +- **-bias image** Output an image containing the estimated bias field Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -234,9 +234,9 @@ Options for importing the diffusion gradient table Options common to all dwibiascorrect algorithms ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask image** Manually provide a mask image for bias field estimation +- **-mask image** Manually provide an input mask image for bias field estimation -- **-bias image** Output the estimated bias field +- **-bias image** Output an image containing the estimated bias field Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwinormalise.rst b/docs/reference/commands/dwinormalise.rst index a228bb5df9..af3b195525 100644 --- a/docs/reference/commands/dwinormalise.rst +++ b/docs/reference/commands/dwinormalise.rst @@ -99,7 +99,7 @@ Usage - *input_dir*: The input directory containing all DWI images - *mask_dir*: Input directory containing brain masks, corresponding to one per input image (with the same file name prefix) - *output_dir*: The output directory containing all of the intensity normalised DWI images -- *fa_template*: The output population specific FA template, which is threshold to estimate a white matter mask +- *fa_template*: The output population-specific FA template, which is thresholded to estimate a white matter mask - *wm_mask*: The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation Description diff --git a/docs/reference/commands/mrregister.rst b/docs/reference/commands/mrregister.rst index ec2052b0ec..421467d07a 100644 --- a/docs/reference/commands/mrregister.rst +++ b/docs/reference/commands/mrregister.rst @@ -69,7 +69,7 @@ Rigid registration options - **-rigid_metric type** valid choices are: diff (intensity differences), Default: diff -- **-rigid_metric.diff.estimator type** Valid choices are: l1 (least absolute: \|x\|), l2 (ordinary least squares), lp (least powers: \|x\|^1.2), Default: l2 +- **-rigid_metric.diff.estimator type** Valid choices are: l1 (least absolute: \|x\|), l2 (ordinary least squares), lp (least powers: \|x\|^1.2), none (no robust estimator). Default: l2 - **-rigid_lmax num** explicitly set the lmax to be used per scale factor in rigid FOD registration. By default FOD registration will use lmax 0,2,4 with default scale factors 0.25,0.5,1.0 respectively. Note that no reorientation will be performed with lmax = 0. diff --git a/docs/reference/commands/mrtrix_cleanup.rst b/docs/reference/commands/mrtrix_cleanup.rst index cfeb5ffd03..cafaefdf09 100644 --- a/docs/reference/commands/mrtrix_cleanup.rst +++ b/docs/reference/commands/mrtrix_cleanup.rst @@ -15,7 +15,7 @@ Usage mrtrix_cleanup path [ options ] -- *path*: Path from which to commence filesystem search +- *path*: Directory from which to commence filesystem search Description ----------- diff --git a/docs/reference/commands/population_template.rst b/docs/reference/commands/population_template.rst index 01a9cd10e3..43a4b7fdff 100644 --- a/docs/reference/commands/population_template.rst +++ b/docs/reference/commands/population_template.rst @@ -31,7 +31,7 @@ Input, output and general options - **-type** Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear -- **-voxel_size** Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma separated values. +- **-voxel_size** Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values. - **-initial_alignment** Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none". @@ -51,7 +51,7 @@ Input, output and general options - **-aggregate** Measure used to aggregate information from transformed images to the template image. Valid choices: mean, median. Default: mean -- **-aggregation_weights** Comma separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape). +- **-aggregation_weights** Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape). - **-nanmask** Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. Only works if -mask_dir has been input. @@ -81,9 +81,9 @@ Options for the linear registration - **-linear_no_drift_correction** Deactivate correction of template appearance (scale and shear) over iterations -- **-linear_estimator** Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: \|x\|), l2 (ordinary least squares), lp (least powers: \|x\|^1.2), Default: None (no robust estimator used) +- **-linear_estimator** Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: \|x\|), l2 (ordinary least squares), lp (least powers: \|x\|^1.2), none (no robust estimator). Default: none. -- **-rigid_scale** Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: 0.3,0.4,0.6,0.8,1.0,1.0). This and affine_scale implicitly define the number of template levels +- **-rigid_scale** Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: 0.3,0.4,0.6,0.8,1.0,1.0). This and affine_scale implicitly define the number of template levels - **-rigid_lmax** Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: 2,2,2,4,4,4). The list must be the same length as the linear_scale factor list diff --git a/docs/reference/commands/responsemean.rst b/docs/reference/commands/responsemean.rst index 4df64f678b..32e8436c7b 100644 --- a/docs/reference/commands/responsemean.rst +++ b/docs/reference/commands/responsemean.rst @@ -15,8 +15,8 @@ Usage responsemean inputs output [ options ] -- *inputs*: The input response functions -- *output*: The output mean response function +- *inputs*: The input response function files +- *output*: The output mean response function file Description ----------- diff --git a/docs/reference/environment_variables.rst b/docs/reference/environment_variables.rst index 29f968b6af..bc6f5554ad 100644 --- a/docs/reference/environment_variables.rst +++ b/docs/reference/environment_variables.rst @@ -68,6 +68,15 @@ List of MRtrix3 environment variables (e.g. [ 0 0 0 1000 ] for a b=1000 acquisition) to b=0 due to b-value scaling. +.. envvar:: MRTRIX_PRESERVE_TMPFILE + + This variable decides whether the temporary piped image + should be preserved rather than the usual behaviour of + deletion at command completion. + For example, in case of piped commands from Python API, + it is necessary to retain the temp files until all + the piped commands are executed. + .. envvar:: MRTRIX_QUIET Do not display information messages or progress status. This has diff --git a/lib/mrtrix3/_5ttgen/freesurfer.py b/lib/mrtrix3/_5ttgen/freesurfer.py index 2577693d32..e3a8c6c18e 100644 --- a/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/lib/mrtrix3/_5ttgen/freesurfer.py @@ -26,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input FreeSurfer parcellation image (any image containing \'aseg\' in its name)') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') options = parser.add_argument_group('Options specific to the \'freesurfer\' algorithm') - options.add_argument('-lut', type=app.Parser.FileIn(), help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') + options.add_argument('-lut', type=app.Parser.FileIn(), metavar='file', help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') diff --git a/lib/mrtrix3/_5ttgen/fsl.py b/lib/mrtrix3/_5ttgen/fsl.py index c3c132fe3d..e6383f85fe 100644 --- a/lib/mrtrix3/_5ttgen/fsl.py +++ b/lib/mrtrix3/_5ttgen/fsl.py @@ -31,7 +31,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') options = parser.add_argument_group('Options specific to the \'fsl\' algorithm') options.add_argument('-t2', type=app.Parser.ImageIn(), metavar='image', help='Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST') - options.add_argument('-mask', type=app.Parser.ImageIn(), help='Manually provide a brain mask, rather than deriving one in the script') + options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Manually provide a brain mask, rather than deriving one in the script') options.add_argument('-premasked', action='store_true', help='Indicate that brain masking has already been applied to the input image') parser.flag_mutually_exclusive_options( [ 'mask', 'premasked' ] ) diff --git a/lib/mrtrix3/_5ttgen/hsvs.py b/lib/mrtrix3/_5ttgen/hsvs.py index c25e71c067..eb226c328c 100644 --- a/lib/mrtrix3/_5ttgen/hsvs.py +++ b/lib/mrtrix3/_5ttgen/hsvs.py @@ -36,7 +36,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_synopsis('Generate a 5TT image based on Hybrid Surface and Volume Segmentation (HSVS), using FreeSurfer and FSL tools') parser.add_argument('input', type=app.Parser.DirectoryIn(), help='The input FreeSurfer subject directory') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') - parser.add_argument('-template', type=app.Parser.ImageIn(), help='Provide an image that will form the template for the generated 5TT image') + parser.add_argument('-template', type=app.Parser.ImageIn(), metavar='image', help='Provide an image that will form the template for the generated 5TT image') parser.add_argument('-hippocampi', choices=HIPPOCAMPI_CHOICES, help='Select method to be used for hippocampi (& amygdalae) segmentation; options are: ' + ','.join(HIPPOCAMPI_CHOICES)) parser.add_argument('-thalami', choices=THALAMI_CHOICES, help='Select method to be used for thalamic segmentation; options are: ' + ','.join(THALAMI_CHOICES)) parser.add_argument('-white_stem', action='store_true', help='Classify the brainstem as white matter') diff --git a/lib/mrtrix3/dwi2mask/b02template.py b/lib/mrtrix3/dwi2mask/b02template.py index 1b9420fe25..44c4942c16 100644 --- a/lib/mrtrix3/dwi2mask/b02template.py +++ b/lib/mrtrix3/dwi2mask/b02template.py @@ -53,7 +53,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('b02template', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Register the mean b=0 image to a T2-weighted template to back-propagate a brain mask') - parser.add_description('This script currently assumes that the template image provided via the -template option ' + parser.add_description('This script currently assumes that the template image provided via the first input to the -template option ' 'is T2-weighted, and can therefore be trivially registered to a mean b=0 image.') parser.add_description('Command-line option -ants_options can be used with either the "antsquick" or "antsfull" software options. ' 'In both cases, image dimensionality is assumed to be 3, and so this should be omitted from the user-specified options.' @@ -71,7 +71,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options.add_argument('-software', choices=SOFTWARES, help='The software to use for template registration; options are: ' + ','.join(SOFTWARES) + '; default is ' + DEFAULT_SOFTWARE) options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image to which the input data will be registered, and the mask to be projected to the input image. The template image should be T2-weighted.') ants_options = parser.add_argument_group('Options applicable when using the ANTs software for registration') - ants_options.add_argument('-ants_options', help='Provide options to be passed to the ANTs registration command (see Description)') + ants_options.add_argument('-ants_options', metavar='" ANTsOptions"', help='Provide options to be passed to the ANTs registration command (see Description)') fsl_options = parser.add_argument_group('Options applicable when using the FSL software for registration') fsl_options.add_argument('-flirt_options', metavar='" FlirtOptions"', help='Command-line options to pass to the FSL flirt command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to flirt)') fsl_options.add_argument('-fnirt_config', type=app.Parser.FileIn(), metavar='file', help='Specify a FNIRT configuration file for registration') diff --git a/lib/mrtrix3/dwi2mask/consensus.py b/lib/mrtrix3/dwi2mask/consensus.py index 372f913d2a..040bc030a2 100644 --- a/lib/mrtrix3/dwi2mask/consensus.py +++ b/lib/mrtrix3/dwi2mask/consensus.py @@ -26,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the "consensus" algorithm') options.add_argument('-algorithms', nargs='+', help='Provide a list of dwi2mask algorithms that are to be utilised') - options.add_argument('-masks', type=app.Parser.ImageOut(), help='Export a 4D image containing the individual algorithm masks') + options.add_argument('-masks', type=app.Parser.ImageOut(), metavar='image', help='Export a 4D image containing the individual algorithm masks') options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') options.add_argument('-threshold', type=float, default=DEFAULT_THRESHOLD, help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: ' + str(DEFAULT_THRESHOLD) + ')') diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index ec34c045d7..fc365fed05 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -29,7 +29,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the \'fslbet\' algorithm') options.add_argument('-bet_f', type=float, help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') options.add_argument('-bet_g', type=float, help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') - options.add_argument('-bet_c', type=app.Parser.SequenceFloat(), nargs=3, metavar='', help='Centre-of-gravity (voxels not mm) of initial mesh surface') + options.add_argument('-bet_c', type=app.Parser.SequenceFloat(), metavar='i,j,k', help='Centre-of-gravity (voxels not mm) of initial mesh surface') options.add_argument('-bet_r', type=float, help='Head radius (mm not voxels); initial surface sphere is set to half of this') options.add_argument('-rescale', action='store_true', help='Rescale voxel size provided to BET to 1mm isotropic; can improve results for rodent data') diff --git a/lib/mrtrix3/dwi2mask/mean.py b/lib/mrtrix3/dwi2mask/mean.py index 042dc5ba8b..03d86f290c 100644 --- a/lib/mrtrix3/dwi2mask/mean.py +++ b/lib/mrtrix3/dwi2mask/mean.py @@ -24,7 +24,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'mean\' algorithm') - options.add_argument('-shells', help='Comma separated list of shells to be included in the volume averaging') + options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='Comma separated list of shells to be included in the volume averaging') options.add_argument('-clean_scale', type=int, default=DEFAULT_CLEAN_SCALE, @@ -46,7 +46,7 @@ def needs_mean_bzero(): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable - run.command(('dwiextract input.mif - -shells ' + app.ARGS.shells + ' | mrmath -' \ + run.command(('dwiextract input.mif - -shells ' + ','.join(str(f) for f in app.ARGS.shells) + ' | mrmath -' \ if app.ARGS.shells \ else 'mrmath input.mif') + ' mean - -axis 3 |' diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index 7feb0fb0cb..d042ed921c 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -26,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'trace\' algorithm') - options.add_argument('-shells', type=app.Parser.SequenceFloat(), help='Comma-separated list of shells used to generate trace-weighted images for masking') + options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='Comma-separated list of shells used to generate trace-weighted images for masking') options.add_argument('-clean_scale', type=int, default=DEFAULT_CLEAN_SCALE, diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index 46e1db08dc..e62dd07d76 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -33,8 +33,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable condition='If -wm_algo option is not used') parser.add_argument('input', type=app.Parser.ImageIn(), help='Input DWI dataset') parser.add_argument('out_sfwm', type=app.Parser.FileOut(), help='Output single-fibre WM response function text file') - parser.add_argument('out_gm', type=app.Parser.ImageOut(), help='Output GM response function text file') - parser.add_argument('out_csf', type=app.Parser.ImageOut(), help='Output CSF response function text file') + parser.add_argument('out_gm', type=app.Parser.FileOut(), help='Output GM response function text file') + parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response function text file') options = parser.add_argument_group('Options for the \'dhollander\' algorithm') options.add_argument('-erode', type=int, default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') options.add_argument('-fa', type=float, default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') @@ -83,18 +83,16 @@ def execute(): #pylint: disable=unused-variable bvalues_option = ' -shells ' + ','.join(map(str,bvalues)) # Get lmax information (if provided). + sfwm_lmax_option = '' sfwm_lmax = [ ] if app.ARGS.lmax: sfwm_lmax = app.ARGS.lmax - if not len(sfwm_lmax) == len(bvalues): + if len(sfwm_lmax) != len(bvalues): raise MRtrixError('Number of lmax\'s (' + str(len(sfwm_lmax)) + ', as supplied to the -lmax option: ' + ','.join(map(str,sfwm_lmax)) + ') does not match number of unique b-values.') - for sfl in sfwm_lmax: - if sfl%2: - raise MRtrixError('Values supplied to the -lmax option must be even.') - if sfl<0: - raise MRtrixError('Values supplied to the -lmax option must be non-negative.') - sfwm_lmax_option = '' - if sfwm_lmax: + if any(sfl%2 for sfl in sfwm_lmax): + raise MRtrixError('Values supplied to the -lmax option must be even.') + if any(sfl<0 for sfl in sfwm_lmax): + raise MRtrixError('Values supplied to the -lmax option must be non-negative.') sfwm_lmax_option = ' -lmax ' + ','.join(map(str,sfwm_lmax)) diff --git a/lib/mrtrix3/dwi2response/manual.py b/lib/mrtrix3/dwi2response/manual.py index a2d27271d0..56af4b58b6 100644 --- a/lib/mrtrix3/dwi2response/manual.py +++ b/lib/mrtrix3/dwi2response/manual.py @@ -27,7 +27,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('in_voxels', type=app.Parser.ImageIn(), help='Input voxel selection mask') parser.add_argument('output', type=app.Parser.FileOut(), help='Output response function text file') options = parser.add_argument_group('Options specific to the \'manual\' algorithm') - options.add_argument('-dirs', type=app.Parser.ImageIn(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') + options.add_argument('-dirs', type=app.Parser.ImageIn(), metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') @@ -59,28 +59,24 @@ def supports_mask(): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable shells = [ int(round(float(x))) for x in image.mrinfo('dwi.mif', 'shell_bvalues').split() ] + bvalues_option = ' -shells ' + ','.join(map(str,shells)) # Get lmax information (if provided) - lmax = [ ] + lmax_option = '' if app.ARGS.lmax: - lmax = app.ARGS.lmax - if not len(lmax) == len(shells): - raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(lmax)) + ') does not match number of b-value shells (' + str(len(shells)) + ')') - for shell_l in lmax: - if shell_l % 2: - raise MRtrixError('Values for lmax must be even') - if shell_l < 0: - raise MRtrixError('Values for lmax must be non-negative') + if len(app.ARGS.lmax) != len(shells): + raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(app.ARGS.lmax)) + ') does not match number of b-value shells (' + str(len(shells)) + ')') + if any(l % 2 for l in app.ARGS.lmax): + raise MRtrixError('Values for lmax must be even') + if any(l < 0 for l in app.ARGS.lmax): + raise MRtrixError('Values for lmax must be non-negative') + lmax_option = ' -lmax ' + ','.join(map(str,app.ARGS.lmax)) # Do we have directions, or do we need to calculate them? if not os.path.exists('dirs.mif'): run.command('dwi2tensor dwi.mif - -mask in_voxels.mif | tensor2metric - -vector dirs.mif') # Get response function - bvalues_option = ' -shells ' + ','.join(map(str,shells)) - lmax_option = '' - if lmax: - lmax_option = ' -lmax ' + ','.join(map(str,lmax)) run.command('amp2response dwi.mif in_voxels.mif dirs.mif response.txt' + bvalues_option + lmax_option) run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index cfa80fe4da..611f9fee4a 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -34,7 +34,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('out_gm', type=app.Parser.FileOut(), help='Output GM response text file') parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response text file') options = parser.add_argument_group('Options specific to the \'msmt_5tt\' algorithm') - options.add_argument('-dirs', type=app.Parser.ImageIn(), help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') + options.add_argument('-dirs', type=app.Parser.ImageIn(), metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') options.add_argument('-fa', type=float, default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') options.add_argument('-pvf', type=float, default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, default='tournier', help='dwi2response algorithm to use for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + '; default: tournier)') @@ -88,16 +88,15 @@ def execute(): #pylint: disable=unused-variable app.warn('Less than three b-values; response functions will not be applicable in resolving three tissues using MSMT-CSD algorithm') # Get lmax information (if provided) - wm_lmax = [ ] + sfwm_lmax_option = '' if app.ARGS.lmax: - wm_lmax = app.ARGS.lmax - if not len(wm_lmax) == len(shells): - raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(wm_lmax)) + ') does not match number of b-values (' + str(len(shells)) + ')') - for shell_l in wm_lmax: - if shell_l % 2: - raise MRtrixError('Values for lmax must be even') - if shell_l < 0: - raise MRtrixError('Values for lmax must be non-negative') + if len(app.ARGS.lmax) != len(shells): + raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(app.ARGS.lmax)) + ') does not match number of b-values (' + str(len(shells)) + ')') + if any(l % 2 for l in app.ARGS.lmax): + raise MRtrixError('Values for lmax must be even') + if any(l < 0 for l in app.ARGS.lmax): + raise MRtrixError('Values for lmax must be non-negative') + sfwm_lmax_option = ' -lmax ' + ','.join(map(str,app.ARGS.lmax)) run.command('dwi2tensor dwi.mif - -mask mask.mif | tensor2metric - -fa fa.mif -vector vector.mif') if not os.path.exists('dirs.mif'): @@ -143,9 +142,6 @@ def execute(): #pylint: disable=unused-variable # For each of the three tissues, generate a multi-shell response bvalues_option = ' -shells ' + ','.join(map(str,shells)) - sfwm_lmax_option = '' - if wm_lmax: - sfwm_lmax_option = ' -lmax ' + ','.join(map(str,wm_lmax)) run.command('amp2response dwi.mif wm_sf_mask.mif dirs.mif wm.txt' + bvalues_option + sfwm_lmax_option) run.command('amp2response dwi.mif gm_mask.mif dirs.mif gm.txt' + bvalues_option + ' -isotropic') run.command('amp2response dwi.mif csf_mask.mif dirs.mif csf.txt' + bvalues_option + ' -isotropic') diff --git a/lib/mrtrix3/run.py b/lib/mrtrix3/run.py index 572f7f2864..3b70dda75b 100644 --- a/lib/mrtrix3/run.py +++ b/lib/mrtrix3/run.py @@ -79,7 +79,8 @@ def __init__(self): self._scratch_dir = None self.verbosity = 1 - self.env['MRTRIX_DELETE_TMPFILE'] = 'no' + # Ensures that temporary piped images are not deleted automatically by MRtrix3 binary commands + self.env['MRTRIX_PRESERVE_TMPFILE'] = 'yes' # Acquire a unique index # This ensures that if command() is executed in parallel using different threads, they will From 2894b1dc392dd56aa068b359544ce199cde0422c Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 10 Aug 2023 15:14:37 +1000 Subject: [PATCH 18/75] Move handling of envvar MRTRIX_PRESERVE_TMPFILE --- core/image_io/pipe.cpp | 15 ++++++++++++++- core/signal_handler.cpp | 16 +++------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/core/image_io/pipe.cpp b/core/image_io/pipe.cpp index 3ef3e50d3e..99090c16fa 100644 --- a/core/image_io/pipe.cpp +++ b/core/image_io/pipe.cpp @@ -55,7 +55,20 @@ namespace MR } } - bool Pipe::delete_piped_images = true; + //ENVVAR name: MRTRIX_PRESERVE_TMPFILE + //ENVVAR This variable decides whether the temporary piped image + //ENVVAR should be preserved rather than the usual behaviour of + //ENVVAR deletion at command completion. + //ENVVAR For example, in case of piped commands from Python API, + //ENVVAR it is necessary to retain the temp files until all + //ENVVAR the piped commands are executed. + namespace { + bool preserve_tmpfile() { + const char* const MRTRIX_PRESERVE_TMPFILE = getenv("MRTRIX_PRESERVE_TMPFILE"); + return (!(MRTRIX_PRESERVE_TMPFILE && to(MRTRIX_PRESERVE_TMPFILE))); + } + } + bool Pipe::delete_piped_images = !preserve_tmpfile(); } } diff --git a/core/signal_handler.cpp b/core/signal_handler.cpp index 0b48a18ced..51f782a361 100644 --- a/core/signal_handler.cpp +++ b/core/signal_handler.cpp @@ -135,19 +135,9 @@ namespace MR void mark_file_for_deletion (const std::string& filename) { - //ENVVAR name: MRTRIX_PRESERVE_TMPFILE - //ENVVAR This variable decides whether the temporary piped image - //ENVVAR should be preserved rather than the usual behaviour of - //ENVVAR deletion at command completion. - //ENVVAR For example, in case of piped commands from Python API, - //ENVVAR it is necessary to retain the temp files until all - //ENVVAR the piped commands are executed. - const char* const MRTRIX_PRESERVE_TMPFILE = getenv("MRTRIX_PRESERVE_TMPFILE"); - if (!MRTRIX_PRESERVE_TMPFILE || !to(MRTRIX_PRESERVE_TMPFILE)) { - while (!flag.test_and_set()); - marked_files.push_back (filename); - flag.clear(); - } + while (!flag.test_and_set()); + marked_files.push_back (filename); + flag.clear(); } void unmark_file_for_deletion (const std::string& filename) From 294ea57889381aae3a6b2dcaafecdbefea3a11c0 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 10 Aug 2023 15:50:46 +1000 Subject: [PATCH 19/75] Fix pylint warnings for python_cmd_changes branch --- bin/dwicat | 2 +- bin/labelsgmfix | 2 +- bin/mask2glass | 2 +- bin/mrtrix_cleanup | 2 +- bin/population_template | 2 +- bin/responsemean | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/dwicat b/bin/dwicat index 9dd8803291..80150be6e1 100755 --- a/bin/dwicat +++ b/bin/dwicat @@ -22,7 +22,7 @@ import json, shutil def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Lena Dorfschmidt (ld548@cam.ac.uk) and Jakub Vohryzek (jakub.vohryzek@queens.ox.ac.uk) and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Concatenating multiple DWI series accounting for differential intensity scaling') diff --git a/bin/labelsgmfix b/bin/labelsgmfix index 82affcbc20..03b8108631 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -30,7 +30,7 @@ import math, os def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('In a FreeSurfer parcellation image, replace the sub-cortical grey matter structure delineations using FSL FIRST') cmdline.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) diff --git a/bin/mask2glass b/bin/mask2glass index 193e81319e..665878fab8 100755 --- a/bin/mask2glass +++ b/bin/mask2glass @@ -16,7 +16,7 @@ # For more details, see http://www.mrtrix.org/. def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Remika Mito (remika.mito@florey.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Create a glass brain from mask input') cmdline.add_description('The output of this command is a glass brain image, which can be viewed ' diff --git a/bin/mrtrix_cleanup b/bin/mrtrix_cleanup index feb5d63d09..57ff9bbe3d 100755 --- a/bin/mrtrix_cleanup +++ b/bin/mrtrix_cleanup @@ -23,7 +23,7 @@ POSTFIXES = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ] def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Clean up residual temporary files & scratch directories from MRtrix3 commands') diff --git a/bin/population_template b/bin/population_template index 42aee18761..e30459520d 100755 --- a/bin/population_template +++ b/bin/population_template @@ -45,7 +45,7 @@ LEAVE_ONE_OUT = ['0', '1', 'auto'] IMAGEEXT = ['mif', 'nii', 'mih', 'mgh', 'mgz', 'img', 'hdr'] def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('David Raffelt (david.raffelt@florey.edu.au) & Max Pietsch (maximilian.pietsch@kcl.ac.uk) & Thijs Dhollander (thijs.dhollander@gmail.com)') cmdline.set_synopsis('Generates an unbiased group-average template from a series of images') diff --git a/bin/responsemean b/bin/responsemean index f0d59f1f8c..f07522e779 100755 --- a/bin/responsemean +++ b/bin/responsemean @@ -21,7 +21,7 @@ import math, os, sys def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') cmdline.set_synopsis('Calculate the mean response function from a set of text files') cmdline.add_description('Example usage: ' + os.path.basename(sys.argv[0]) + ' input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt') From 70c154edb26926ef7a5be4dfa3bf25a3b38cb923 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 10 Aug 2023 15:55:17 +1000 Subject: [PATCH 20/75] Python: Move some functions from path to utils module In particular, it is desired for function make_temporary() to be accessible from within the app module. --- lib/mrtrix3/dwinormalise/group.py | 8 ++--- lib/mrtrix3/image.py | 4 +-- lib/mrtrix3/path.py | 55 +------------------------------ lib/mrtrix3/utils.py | 54 +++++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 61 deletions(-) diff --git a/lib/mrtrix3/dwinormalise/group.py b/lib/mrtrix3/dwinormalise/group.py index c8263a8bfd..b76af7887c 100644 --- a/lib/mrtrix3/dwinormalise/group.py +++ b/lib/mrtrix3/dwinormalise/group.py @@ -15,7 +15,7 @@ import os, shlex from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, path, run, utils @@ -83,7 +83,7 @@ def __init__(self, filename, prefix, mask_filename = ''): app.make_scratch_dir() app.goto_scratch_dir() - path.make_dir('fa') + utils.make_dir('fa') progress = app.ProgressBar('Computing FA images', len(input_list)) for i in input_list: run.command('dwi2tensor ' + shlex.quote(os.path.join(input_dir, i.filename)) + ' -mask ' + shlex.quote(os.path.join(mask_dir, i.mask_filename)) + ' - | tensor2metric - -fa ' + os.path.join('fa', i.prefix + '.mif')) @@ -107,8 +107,8 @@ def __init__(self, filename, prefix, mask_filename = ''): run.command('mrthreshold fa_template.mif -abs ' + app.ARGS.fa_threshold + ' template_wm_mask.mif') progress = app.ProgressBar('Intensity normalising subject images', len(input_list)) - path.make_dir(path.from_user(app.ARGS.output_dir, False)) - path.make_dir('wm_mask_warped') + utils.make_dir(path.from_user(app.ARGS.output_dir, False)) + utils.make_dir('wm_mask_warped') for i in input_list: run.command('mrtransform template_wm_mask.mif -interp nearest -warp_full ' + os.path.join('warps', i.prefix + '.mif') + ' ' + os.path.join('wm_mask_warped', i.prefix + '.mif') + ' -from 2 -template ' + os.path.join('fa', i.prefix + '.mif')) run.command('dwinormalise individual ' + shlex.quote(os.path.join(input_dir, i.filename)) + ' ' + os.path.join('wm_mask_warped', i.prefix + '.mif') + ' temp.mif') diff --git a/lib/mrtrix3/image.py b/lib/mrtrix3/image.py index 83becc5c1f..37264e7e1f 100644 --- a/lib/mrtrix3/image.py +++ b/lib/mrtrix3/image.py @@ -28,8 +28,8 @@ # Class for importing header information from an image file for reading class Header: def __init__(self, image_path): - from mrtrix3 import app, path, run #pylint: disable=import-outside-toplevel - filename = path.name_temporary('json') + from mrtrix3 import app, run, utils #pylint: disable=import-outside-toplevel + filename = utils.name_temporary('json') command = [ run.exe_name(run.version_match('mrinfo')), image_path, '-json_all', filename, '-nodelete' ] if app.VERBOSITY > 1: app.console('Loading header for image file \'' + image_path + '\'') diff --git a/lib/mrtrix3/path.py b/lib/mrtrix3/path.py index 485730b4b7..cc618fefee 100644 --- a/lib/mrtrix3/path.py +++ b/lib/mrtrix3/path.py @@ -17,8 +17,7 @@ -import ctypes, errno, inspect, os, random, shlex, shutil, string, subprocess, time -from mrtrix3 import CONFIG +import ctypes, inspect, os, shlex, shutil, subprocess, time @@ -64,58 +63,6 @@ def from_user(filename, escape=True): #pylint: disable=unused-variable -# Make a directory if it doesn't exist; don't do anything if it does already exist -def make_dir(path): #pylint: disable=unused-variable - from mrtrix3 import app #pylint: disable=import-outside-toplevel - try: - os.makedirs(path) - app.debug('Created directory ' + path) - except OSError as exception: - if exception.errno != errno.EEXIST: - raise - app.debug('Directory \'' + path + '\' already exists') - - - -# Make a temporary empty file / directory with a unique name -# If the filesystem path separator is provided as the 'suffix' input, then the function will generate a new -# directory rather than a file. -def make_temporary(suffix): #pylint: disable=unused-variable - from mrtrix3 import app #pylint: disable=import-outside-toplevel - is_directory = suffix in '\\/' and len(suffix) == 1 - while True: - temp_path = name_temporary(suffix) - try: - if is_directory: - os.makedirs(temp_path) - else: - with open(temp_path, 'a', encoding='utf-8'): - pass - app.debug(temp_path) - return temp_path - except OSError as exception: - if exception.errno != errno.EEXIST: - raise - - - -# Get an appropriate location and name for a new temporary file / directory -# Note: Doesn't actually create anything; just gives a unique name that won't over-write anything. -# If you want to create a temporary file / directory, use the make_temporary() function above. -def name_temporary(suffix): #pylint: disable=unused-variable - from mrtrix3 import app #pylint: disable=import-outside-toplevel - dir_path = CONFIG['TmpFileDir'] if 'TmpFileDir' in CONFIG else (app.SCRATCH_DIR if app.SCRATCH_DIR else os.getcwd()) - prefix = CONFIG['TmpFilePrefix'] if 'TmpFilePrefix' in CONFIG else 'mrtrix-tmp-' - full_path = dir_path - suffix = suffix.lstrip('.') - while os.path.exists(full_path): - random_string = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for x in range(6)) - full_path = os.path.join(dir_path, prefix + random_string + '.' + suffix) - app.debug(full_path) - return full_path - - - # Determine the name of a sub-directory containing additional data / source files for a script # This can be algorithm files in lib/mrtrix3/, or data files in share/mrtrix3/ # This function appears here rather than in the algorithm module as some scripts may diff --git a/lib/mrtrix3/utils.py b/lib/mrtrix3/utils.py index 09061497b3..6e76926a86 100644 --- a/lib/mrtrix3/utils.py +++ b/lib/mrtrix3/utils.py @@ -17,7 +17,8 @@ -import platform, re +import errno, os, platform, random, re, string +from mrtrix3 import CONFIG @@ -106,3 +107,54 @@ def decode(line): else: res[name] = var.split() return res + + + +# Get an appropriate location and name for a new temporary file / directory +# Note: Doesn't actually create anything; just gives a unique name that won't over-write anything. +# If you want to create a temporary file / directory, use the make_temporary() function above. +def name_temporary(suffix): #pylint: disable=unused-variable + from mrtrix3 import app #pylint: disable=import-outside-toplevel + dir_path = CONFIG['TmpFileDir'] if 'TmpFileDir' in CONFIG else (app.SCRATCH_DIR if app.SCRATCH_DIR else os.getcwd()) + prefix = CONFIG['TmpFilePrefix'] if 'TmpFilePrefix' in CONFIG else 'mrtrix-tmp-' + full_path = dir_path + suffix = suffix.lstrip('.') + while os.path.exists(full_path): + random_string = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for x in range(6)) + full_path = os.path.join(dir_path, prefix + random_string + '.' + suffix) + app.debug(full_path) + return full_path + + +# Make a temporary empty file / directory with a unique name +# If the filesystem path separator is provided as the 'suffix' input, then the function will generate a new +# directory rather than a file. +def make_temporary(suffix): #pylint: disable=unused-variable + from mrtrix3 import app #pylint: disable=import-outside-toplevel + is_directory = suffix in '\\/' and len(suffix) == 1 + while True: + temp_path = name_temporary(suffix) + try: + if is_directory: + os.makedirs(temp_path) + else: + with open(temp_path, 'a', encoding='utf-8'): + pass + app.debug(temp_path) + return temp_path + except OSError as exception: + if exception.errno != errno.EEXIST: + raise + + + +# Make a directory if it doesn't exist; don't do anything if it does already exist +def make_dir(path): #pylint: disable=unused-variable + from mrtrix3 import app #pylint: disable=import-outside-toplevel + try: + os.makedirs(path) + app.debug('Created directory ' + path) + except OSError as exception: + if exception.errno != errno.EEXIST: + raise + app.debug('Directory \'' + path + '\' already exists') From e9393d729ee556a096f546402cacaf2664a0a979 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 10 Aug 2023 16:55:15 +1000 Subject: [PATCH 21/75] First functional version of image piping with Python scripts --- bin/dwi2mask | 6 ++-- bin/dwi2response | 13 ++++++-- bin/dwibiascorrect | 6 ++-- bin/dwicat | 12 ++++--- bin/dwifslpreproc | 9 ++++-- bin/dwigradcheck | 6 ++-- bin/dwishellmath | 13 ++++++-- bin/labelsgmfix | 16 +++++++--- bin/mask2glass | 6 ++-- core/image_io/pipe.cpp | 2 +- lib/mrtrix3/_5ttgen/freesurfer.py | 8 +++-- lib/mrtrix3/_5ttgen/fsl.py | 24 +++++++++----- lib/mrtrix3/_5ttgen/gif.py | 8 +++-- lib/mrtrix3/_5ttgen/hsvs.py | 6 ++-- lib/mrtrix3/app.py | 43 ++++++++++++++++++++------ lib/mrtrix3/dwi2response/dhollander.py | 6 +++- lib/mrtrix3/dwi2response/fa.py | 5 ++- lib/mrtrix3/dwi2response/manual.py | 11 +++++-- lib/mrtrix3/dwi2response/msmt_5tt.py | 11 +++++-- lib/mrtrix3/dwi2response/tax.py | 5 ++- lib/mrtrix3/dwi2response/tournier.py | 5 ++- lib/mrtrix3/dwibiascorrect/ants.py | 10 ++++-- lib/mrtrix3/dwibiascorrect/fsl.py | 10 ++++-- lib/mrtrix3/dwinormalise/individual.py | 6 ++-- lib/mrtrix3/run.py | 15 ++++++--- 25 files changed, 194 insertions(+), 68 deletions(-) diff --git a/bin/dwi2mask b/bin/dwi2mask index ec404feddd..f56dc8cd0b 100755 --- a/bin/dwi2mask +++ b/bin/dwi2mask @@ -56,7 +56,8 @@ def execute(): #pylint: disable=unused-variable # Get input data into the scratch directory run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif') - + ' -strides 0,0,0,1' + grad_import_option) + + ' -strides 0,0,0,1' + grad_import_option, + preserve_pipes=True) alg.get_inputs() app.goto_scratch_dir() @@ -96,7 +97,8 @@ def execute(): #pylint: disable=unused-variable + path.from_user(app.ARGS.output) + ' -strides ' + ','.join(str(value) for value in strides), mrconvert_keyval=path.from_user(app.ARGS.input, False), - force=app.FORCE_OVERWRITE) + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/bin/dwi2response b/bin/dwi2response index 71ad5ec82d..c537506883 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -83,13 +83,20 @@ def execute(): #pylint: disable=unused-variable # Get standard input data into the scratch directory if alg.needs_single_shell() or shells_option: app.console('Importing DWI data (' + path.from_user(app.ARGS.input) + ') and selecting b-values...') - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' - -strides 0,0,0,1' + grad_import_option + ' | dwiextract - ' + path.to_scratch('dwi.mif') + shells_option + singleshell_option, show=False) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' - -strides 0,0,0,1' + grad_import_option + ' | ' + 'dwiextract - ' + path.to_scratch('dwi.mif') + shells_option + singleshell_option, + show=False, + preserve_pipes=True) else: # Don't discard b=0 in multi-shell algorithms app.console('Importing DWI data (' + path.from_user(app.ARGS.input) + ')...') - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('dwi.mif') + ' -strides 0,0,0,1' + grad_import_option, show=False) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('dwi.mif') + ' -strides 0,0,0,1' + grad_import_option, + show=False, + preserve_pipes=True) if app.ARGS.mask: app.console('Importing mask (' + path.from_user(app.ARGS.mask) + ')...') - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', show=False) + run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + show=False, + preserve_pipes=True) alg.get_inputs() diff --git a/bin/dwibiascorrect b/bin/dwibiascorrect index ba34aa32d4..66b0172588 100755 --- a/bin/dwibiascorrect +++ b/bin/dwibiascorrect @@ -49,9 +49,11 @@ def execute(): #pylint: disable=unused-variable app.make_scratch_dir() grad_import_option = app.read_dwgrad_import_options() - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif') + grad_import_option) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif') + grad_import_option, + preserve_pipes=True) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit') + run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + preserve_pipes=True) alg.get_inputs() diff --git a/bin/dwicat b/bin/dwicat index 80150be6e1..d920818376 100755 --- a/bin/dwicat +++ b/bin/dwicat @@ -55,7 +55,7 @@ def execute(): #pylint: disable=unused-variable try: if isinstance(dw_scheme[0], list): num_grad_lines = len(dw_scheme) - elif (isinstance(dw_scheme[0], ( int, float))) and len(dw_scheme) >= 4: + elif (isinstance(dw_scheme[0], (int, float))) and len(dw_scheme) >= 4: num_grad_lines = 1 else: raise MRtrixError @@ -95,9 +95,11 @@ def execute(): #pylint: disable=unused-variable # import data to scratch directory app.make_scratch_dir() for index, filename in enumerate(app.ARGS.inputs): - run.command('mrconvert ' + path.from_user(filename) + ' ' + path.to_scratch(str(index) + 'in.mif')) + run.command('mrconvert ' + path.from_user(filename) + ' ' + path.to_scratch(str(index) + 'in.mif'), + preserve_pipes=True) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit') + run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + preserve_pipes=True) app.goto_scratch_dir() # extract b=0 volumes within each input series @@ -151,7 +153,9 @@ def execute(): #pylint: disable=unused-variable with open('result_final.json', 'w', encoding='utf-8') as output_json_file: json.dump(keyval, output_json_file) - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval='result_final.json', force=app.FORCE_OVERWRITE) + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval='result_final.json', + force=app.FORCE_OVERWRITE) diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index 41770abc8f..63fdbf7787 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -251,10 +251,12 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.json_import: json_import_option = ' -json_import ' + path.from_user(app.ARGS.json_import) json_export_option = ' -json_export ' + path.to_scratch('dwi.json', True) - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('dwi.mif') + grad_import_option + json_import_option + json_export_option) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('dwi.mif') + grad_import_option + json_import_option + json_export_option, + preserve_pipes=True) if app.ARGS.se_epi: image.check_3d_nonunity(path.from_user(app.ARGS.se_epi, False)) - run.command('mrconvert ' + path.from_user(app.ARGS.se_epi) + ' ' + path.to_scratch('se_epi.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.se_epi) + ' ' + path.to_scratch('se_epi.mif'), + preserve_pipes=True) if topup_file_userpath: run.function(shutil.copyfile, topup_input_movpar, path.to_scratch('field_movpar.txt', False)) # Can't run field spline coefficients image through mrconvert: @@ -262,7 +264,8 @@ def execute(): #pylint: disable=unused-variable # applytopup requires that these be set, but mrconvert will wipe them run.function(shutil.copyfile, topup_input_fieldcoef, path.to_scratch('field_fieldcoef.nii' + ('.gz' if topup_input_fieldcoef.endswith('.nii.gz') else ''), False)) if app.ARGS.eddy_mask: - run.command('mrconvert ' + path.from_user(app.ARGS.eddy_mask) + ' ' + path.to_scratch('eddy_mask.mif') + ' -datatype bit') + run.command('mrconvert ' + path.from_user(app.ARGS.eddy_mask) + ' ' + path.to_scratch('eddy_mask.mif') + ' -datatype bit', + preserve_pipes=True) app.goto_scratch_dir() diff --git a/bin/dwigradcheck b/bin/dwigradcheck index dc6eaccdec..b112cf0671 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -53,7 +53,8 @@ def execute(): #pylint: disable=unused-variable app.make_scratch_dir() # Make sure the image data can be memory-mapped - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('data.mif') + ' -strides 0,0,0,1 -datatype float32') + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('data.mif') + ' -strides 0,0,0,1 -datatype float32', + preserve_pipes=True) if app.ARGS.grad: shutil.copy(path.from_user(app.ARGS.grad, False), path.to_scratch('grad.b', False)) @@ -61,7 +62,8 @@ def execute(): #pylint: disable=unused-variable shutil.copy(path.from_user(app.ARGS.fslgrad[0], False), path.to_scratch('bvecs', False)) shutil.copy(path.from_user(app.ARGS.fslgrad[1], False), path.to_scratch('bvals', False)) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit') + run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + preserve_pipes=True) app.goto_scratch_dir() diff --git a/bin/dwishellmath b/bin/dwishellmath index a59227595c..718ed00cec 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -48,7 +48,8 @@ def execute(): #pylint: disable=unused-variable app.check_output_path(app.ARGS.output) # import data and gradient table app.make_scratch_dir() - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif') + gradimport + ' -strides 0,0,0,1') + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif') + gradimport + ' -strides 0,0,0,1', + preserve_pipes=True) app.goto_scratch_dir() # run per-shell operations files = [] @@ -59,11 +60,17 @@ def execute(): #pylint: disable=unused-variable if len(files) > 1: # concatenate to output file run.command('mrcat -axis 3 ' + ' '.join(files) + ' out.mif') - run.command('mrconvert out.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert out.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) else: # make a 4D image with one volume app.warn('Only one unique b-value present in DWI data; command mrmath with -axis 3 option may be preferable') - run.command('mrconvert ' + files[0] + ' ' + path.from_user(app.ARGS.output) + ' -axes 0,1,2,-1', mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert ' + files[0] + ' ' + path.from_user(app.ARGS.output) + ' -axes 0,1,2,-1', + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/bin/labelsgmfix b/bin/labelsgmfix index 03b8108631..f7e1673621 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -91,11 +91,16 @@ def execute(): #pylint: disable=unused-variable app.make_scratch_dir() # Get the parcellation and T1 images into the scratch directory, with conversion of the T1 into the correct format for FSL - run.command('mrconvert ' + path.from_user(app.ARGS.parc) + ' ' + path.to_scratch('parc.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.parc) + ' ' + path.to_scratch('parc.mif'), + preserve_pipes=True) if upsample_for_first: - run.command('mrgrid ' + path.from_user(app.ARGS.t1) + ' regrid - -voxel 1.0 -interp sinc | mrcalc - 0.0 -max - | mrconvert - ' + path.to_scratch('T1.nii') + ' -strides -1,+2,+3') + run.command('mrgrid ' + path.from_user(app.ARGS.t1) + ' regrid - -voxel 1.0 -interp sinc | ' + 'mrcalc - 0.0 -max - | ' + 'mrconvert - ' + path.to_scratch('T1.nii') + ' -strides -1,+2,+3', + preserve_pipes=True) else: - run.command('mrconvert ' + path.from_user(app.ARGS.t1) + ' ' + path.to_scratch('T1.nii') + ' -strides -1,+2,+3') + run.command('mrconvert ' + path.from_user(app.ARGS.t1) + ' ' + path.to_scratch('T1.nii') + ' -strides -1,+2,+3', + preserve_pipes=True) app.goto_scratch_dir() @@ -165,7 +170,10 @@ def execute(): #pylint: disable=unused-variable # Insert the new delineations of all SGM structures in a single call # Enforce unsigned integer datatype of output image run.command('mrcalc sgm_new_labels.mif 0.5 -gt sgm_new_labels.mif parc.mif -if result.mif -datatype uint32') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.parc, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval=path.from_user(app.ARGS.parc, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/bin/mask2glass b/bin/mask2glass index 665878fab8..969f0677af 100755 --- a/bin/mask2glass +++ b/bin/mask2glass @@ -39,7 +39,8 @@ def execute(): #pylint: disable=unused-variable # import data to scratch directory app.make_scratch_dir() - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif'), + preserve_pipes=True) app.goto_scratch_dir() dilate_option = ' -npass ' + str(app.ARGS.dilate) @@ -77,7 +78,8 @@ def execute(): #pylint: disable=unused-variable # create output image run.command('mrconvert out.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), - force=app.FORCE_OVERWRITE) + force=app.FORCE_OVERWRITE, + preserve_pipes=True) # Execute the script import mrtrix3 #pylint: disable=wrong-import-position diff --git a/core/image_io/pipe.cpp b/core/image_io/pipe.cpp index 99090c16fa..ab1e5466ee 100644 --- a/core/image_io/pipe.cpp +++ b/core/image_io/pipe.cpp @@ -65,7 +65,7 @@ namespace MR namespace { bool preserve_tmpfile() { const char* const MRTRIX_PRESERVE_TMPFILE = getenv("MRTRIX_PRESERVE_TMPFILE"); - return (!(MRTRIX_PRESERVE_TMPFILE && to(MRTRIX_PRESERVE_TMPFILE))); + return (MRTRIX_PRESERVE_TMPFILE && to(std::string(MRTRIX_PRESERVE_TMPFILE))); } } bool Pipe::delete_piped_images = !preserve_tmpfile(); diff --git a/lib/mrtrix3/_5ttgen/freesurfer.py b/lib/mrtrix3/_5ttgen/freesurfer.py index e3a8c6c18e..579a967b51 100644 --- a/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/lib/mrtrix3/_5ttgen/freesurfer.py @@ -36,7 +36,8 @@ def check_output_paths(): #pylint: disable=unused-variable def get_inputs(): #pylint: disable=unused-variable - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif'), + preserve_pipes=True) if app.ARGS.lut: run.function(shutil.copyfile, path.from_user(app.ARGS.lut, False), path.to_scratch('LUT.txt', False)) @@ -79,4 +80,7 @@ def execute(): #pylint: disable=unused-variable run.command('mrcat cgm.mif sgm.mif wm.mif csf.mif path.mif - -axis 3 | mrconvert - result.mif -datatype float32') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/_5ttgen/fsl.py b/lib/mrtrix3/_5ttgen/fsl.py index e6383f85fe..dc34bce720 100644 --- a/lib/mrtrix3/_5ttgen/fsl.py +++ b/lib/mrtrix3/_5ttgen/fsl.py @@ -44,13 +44,16 @@ def check_output_paths(): #pylint: disable=unused-variable def get_inputs(): #pylint: disable=unused-variable image.check_3d_nonunity(path.from_user(app.ARGS.input, False)) - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif'), + preserve_pipes=True) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit -strides -1,+2,+3') + run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit -strides -1,+2,+3', + preserve_pipes=True) if app.ARGS.t2: if not image.match(path.from_user(app.ARGS.input, False), path.from_user(app.ARGS.t2, False)): raise MRtrixError('Provided T2 image does not match input T1 image') - run.command('mrconvert ' + path.from_user(app.ARGS.t2) + ' ' + path.to_scratch('T2.nii') + ' -strides -1,+2,+3') + run.command('mrconvert ' + path.from_user(app.ARGS.t2) + ' ' + path.to_scratch('T2.nii') + ' -strides -1,+2,+3', + preserve_pipes=True) @@ -211,7 +214,9 @@ def execute(): #pylint: disable=unused-variable fast_gm_output = fsl.find_image(fast_output_prefix + '_pve_1') fast_wm_output = fsl.find_image(fast_output_prefix + '_pve_2') # Step 1: Run LCC on the WM image - run.command('mrthreshold ' + fast_wm_output + ' - -abs 0.001 | maskfilter - connect - -connectivity | mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') + run.command('mrthreshold ' + fast_wm_output + ' - -abs 0.001 | ' + 'maskfilter - connect - -connectivity | ' + 'mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') # Step 2: Generate the images in the same fashion as the old 5ttgen binary used to: # - Preserve CSF as-is # - Preserve SGM, unless it results in a sum of volume fractions greater than 1, in which case clamp @@ -229,6 +234,11 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.nocrop: run.function(os.rename, 'combined_precrop.mif', 'result.mif') else: - run.command('mrmath combined_precrop.mif sum - -axis 3 | mrthreshold - - -abs 0.5 | mrgrid combined_precrop.mif crop result.mif -mask -') - - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrmath combined_precrop.mif sum - -axis 3 | ' + 'mrthreshold - - -abs 0.5 | ' + 'mrgrid combined_precrop.mif crop result.mif -mask -') + + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/_5ttgen/gif.py b/lib/mrtrix3/_5ttgen/gif.py index a0231bebf8..daedd67270 100644 --- a/lib/mrtrix3/_5ttgen/gif.py +++ b/lib/mrtrix3/_5ttgen/gif.py @@ -41,7 +41,8 @@ def check_gif_input(image_path): def get_inputs(): #pylint: disable=unused-variable check_gif_input(path.from_user(app.ARGS.input, False)) - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif'), + preserve_pipes=True) def execute(): #pylint: disable=unused-variable @@ -65,4 +66,7 @@ def execute(): #pylint: disable=unused-variable else: run.command('mrmath 5tt.mif sum - -axis 3 | mrthreshold - - -abs 0.5 | mrgrid 5tt.mif crop result.mif -mask -') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/_5ttgen/hsvs.py b/lib/mrtrix3/_5ttgen/hsvs.py index eb226c328c..418ba2d732 100644 --- a/lib/mrtrix3/_5ttgen/hsvs.py +++ b/lib/mrtrix3/_5ttgen/hsvs.py @@ -144,9 +144,11 @@ def check_output_paths(): #pylint: disable=unused-variable def get_inputs(): #pylint: disable=unused-variable # Most freeSurfer files will be accessed in-place; no need to pre-convert them into the temporary directory # However convert aparc image so that it does not have to be repeatedly uncompressed - run.command('mrconvert ' + path.from_user(os.path.join(app.ARGS.input, 'mri', 'aparc+aseg.mgz'), True) + ' ' + path.to_scratch('aparc.mif', True)) + run.command('mrconvert ' + path.from_user(os.path.join(app.ARGS.input, 'mri', 'aparc+aseg.mgz'), True) + ' ' + path.to_scratch('aparc.mif', True), + preserve_pipes=True) if app.ARGS.template: - run.command('mrconvert ' + path.from_user(app.ARGS.template, True) + ' ' + path.to_scratch('template.mif', True) + ' -axes 0,1,2') + run.command('mrconvert ' + path.from_user(app.ARGS.template, True) + ' ' + path.to_scratch('template.mif', True) + ' -axes 0,1,2', + preserve_pipes=True) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index dddba926d9..90cc42bc17 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -102,6 +102,14 @@ +# Store any input piped images that need to be deleted upon script completion +# rather than when some underlying MRtrix3 command reads them +_STDIN_IMAGES = [] +# Store output piped images that need to be emitted to stdout upon script completion +_STDOUT_IMAGES = [] + + + # Generally preferable to use: # "import mrtrix3" # "mrtrix3.execute()" @@ -252,6 +260,14 @@ def _execute(module): #pylint: disable=unused-variable if not return_code: console('Changing back to original directory (' + WORKING_DIR + ')') os.chdir(WORKING_DIR) + if _STDIN_IMAGES: + debug('Erasing ' + str(len(_STDIN_IMAGES)) + ' piped input images') + for item in _STDIN_IMAGES: + try: + os.remove(item) + debug('Successfully erased "' + item + '"') + except OSError as exc: + debug('Unable to erase "' + item + '": ' + str(exc)) if SCRATCH_DIR: if DO_CLEANUP: if not return_code: @@ -263,6 +279,9 @@ def _execute(module): #pylint: disable=unused-variable SCRATCH_DIR = '' else: console('Scratch directory retained; location: ' + SCRATCH_DIR) + if _STDOUT_IMAGES: + debug('Emitting ' + str(len(_STDOUT_IMAGES)) + ' output piped images to stdout') + sys.stdout.write('\n'.join(_STDOUT_IMAGES)) sys.exit(return_code) @@ -1111,23 +1130,23 @@ def __call__(self, input_value): return False try: processed_value = int(processed_value) - except ValueError: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as boolean value"') + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as boolean value"') from exc return bool(processed_value) class SequenceInt: def __call__(self, input_value): try: return [int(i) for i in input_value.split(',')] - except ValueError: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer sequence') + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer sequence') from exc class SequenceFloat: def __call__(self, input_value): try: return [float(i) for i in input_value.split(',')] - except ValueError: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point sequence') + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point sequence') from exc class DirectoryIn: def __call__(self, input_value): @@ -1158,14 +1177,15 @@ def __call__(self, input_value): if input_value == '-': if sys.stdin.isatty(): raise argparse.ArgumentTypeError('Input piped image unavailable from stdin') - input_value = sys.stdin.read().strip() + input_value = sys.stdin.readline().strip() + _STDIN_IMAGES.append(input_value) return input_value class ImageOut: def __call__(self, input_value): if input_value == '-': - result_str = ''.join(random.choice(string.ascii_letters) for _ in range(6)) - input_value = 'mrtrix-tmp-' + result_str + '.mif' + input_value = utils.name_temporary('mif') + _STDOUT_IMAGES.append(input_value) return input_value class TracksIn(FileIn): @@ -1260,4 +1280,9 @@ def handler(signum, _frame): SCRATCH_DIR = '' else: sys.stderr.write(EXEC_NAME + ': ' + ANSI.console + 'Scratch directory retained; location: ' + SCRATCH_DIR + ANSI.clear + '\n') + for item in _STDIN_IMAGES: + try: + os.remove(item) + except OSError: + pass os._exit(signum) # pylint: disable=protected-access diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index e62dd07d76..e607186791 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -291,5 +291,9 @@ def execute(): #pylint: disable=unused-variable run.function(shutil.copyfile, 'response_gm.txt', path.from_user(app.ARGS.out_gm, False), show=False) run.function(shutil.copyfile, 'response_csf.txt', path.from_user(app.ARGS.out_csf, False), show=False) if app.ARGS.voxels: - run.command('mrconvert check_voxels.mif ' + path.from_user(app.ARGS.voxels), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE, show=False) + run.command('mrconvert check_voxels.mif ' + path.from_user(app.ARGS.voxels), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True, + show=False) app.console('-------') diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index c6dfb86f44..52dab35253 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -76,4 +76,7 @@ def execute(): #pylint: disable=unused-variable run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/manual.py b/lib/mrtrix3/dwi2response/manual.py index 56af4b58b6..aa6cdf50dd 100644 --- a/lib/mrtrix3/dwi2response/manual.py +++ b/lib/mrtrix3/dwi2response/manual.py @@ -41,9 +41,11 @@ def get_inputs(): #pylint: disable=unused-variable if os.path.exists(mask_path): app.warn('-mask option is ignored by algorithm \'manual\'') os.remove(mask_path) - run.command('mrconvert ' + path.from_user(app.ARGS.in_voxels) + ' ' + path.to_scratch('in_voxels.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.in_voxels) + ' ' + path.to_scratch('in_voxels.mif'), + preserve_pipes=True) if app.ARGS.dirs: - run.command('mrconvert ' + path.from_user(app.ARGS.dirs) + ' ' + path.to_scratch('dirs.mif') + ' -strides 0,0,0,1') + run.command('mrconvert ' + path.from_user(app.ARGS.dirs) + ' ' + path.to_scratch('dirs.mif') + ' -strides 0,0,0,1', + preserve_pipes=True) @@ -81,4 +83,7 @@ def execute(): #pylint: disable=unused-variable run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) if app.ARGS.voxels: - run.command('mrconvert in_voxels.mif ' + path.from_user(app.ARGS.voxels), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert in_voxels.mif ' + path.from_user(app.ARGS.voxels), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index 611f9fee4a..7d3068385b 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -50,9 +50,11 @@ def check_output_paths(): #pylint: disable=unused-variable def get_inputs(): #pylint: disable=unused-variable - run.command('mrconvert ' + path.from_user(app.ARGS.in_5tt) + ' ' + path.to_scratch('5tt.mif')) + run.command('mrconvert ' + path.from_user(app.ARGS.in_5tt) + ' ' + path.to_scratch('5tt.mif'), + preserve_pipes=True) if app.ARGS.dirs: - run.command('mrconvert ' + path.from_user(app.ARGS.dirs) + ' ' + path.to_scratch('dirs.mif') + ' -strides 0,0,0,1') + run.command('mrconvert ' + path.from_user(app.ARGS.dirs) + ' ' + path.to_scratch('dirs.mif') + ' -strides 0,0,0,1', + preserve_pipes=True) @@ -152,4 +154,7 @@ def execute(): #pylint: disable=unused-variable # Generate output 4D binary image with voxel selections; RGB as in MSMT-CSD paper run.command('mrcat csf_mask.mif gm_mask.mif wm_sf_mask.mif voxels.mif -axis 3') if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index 5370dbfb80..2dc4d7b60e 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -152,4 +152,7 @@ def execute(): #pylint: disable=unused-variable run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index 8e32cea551..097e453539 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -146,4 +146,7 @@ def execute(): #pylint: disable=unused-variable run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/dwibiascorrect/ants.py b/lib/mrtrix3/dwibiascorrect/ants.py index d3b2e567f5..5dea100344 100644 --- a/lib/mrtrix3/dwibiascorrect/ants.py +++ b/lib/mrtrix3/dwibiascorrect/ants.py @@ -81,6 +81,12 @@ def execute(): #pylint: disable=unused-variable # Common final steps for all algorithms run.command('mrcalc in.mif bias.mif -div result.mif') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) if app.ARGS.bias: - run.command('mrconvert bias.mif ' + path.from_user(app.ARGS.bias), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert bias.mif ' + path.from_user(app.ARGS.bias), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/dwibiascorrect/fsl.py b/lib/mrtrix3/dwibiascorrect/fsl.py index 95842be1d5..dd78ad7335 100644 --- a/lib/mrtrix3/dwibiascorrect/fsl.py +++ b/lib/mrtrix3/dwibiascorrect/fsl.py @@ -65,6 +65,12 @@ def execute(): #pylint: disable=unused-variable # Rather than using a bias field estimate of 1.0 outside the brain mask, zero-fill the # output image outside of this mask run.command('mrcalc in.mif ' + bias_path + ' -div mask.mif -mult result.mif') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) if app.ARGS.bias: - run.command('mrconvert ' + bias_path + ' ' + path.from_user(app.ARGS.bias), mrconvert_keyval=path.from_user(app.ARGS.input, False), force=app.FORCE_OVERWRITE) + run.command('mrconvert ' + bias_path + ' ' + path.from_user(app.ARGS.bias), + mrconvert_keyval=path.from_user(app.ARGS.input, False), + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/dwinormalise/individual.py b/lib/mrtrix3/dwinormalise/individual.py index 92716de89c..9ed0dc11b5 100644 --- a/lib/mrtrix3/dwinormalise/individual.py +++ b/lib/mrtrix3/dwinormalise/individual.py @@ -65,10 +65,12 @@ def execute(): #pylint: disable=unused-variable else: reference_value = float(run.command('dwiextract ' + path.from_user(app.ARGS.input_dwi) + grad_option + ' -bzero - | ' + \ 'mrmath - mean - -axis 3 | ' + \ - 'mrstats - -mask ' + path.from_user(app.ARGS.input_mask) + ' -output median').stdout) + 'mrstats - -mask ' + path.from_user(app.ARGS.input_mask) + ' -output median', + preserve_pipes=True).stdout) multiplier = app.ARGS.intensity / reference_value run.command('mrcalc ' + path.from_user(app.ARGS.input_dwi) + ' ' + str(multiplier) + ' -mult - | ' + \ 'mrconvert - ' + path.from_user(app.ARGS.output_dwi) + grad_option, \ mrconvert_keyval=path.from_user(app.ARGS.input_dwi, False), \ - force=app.FORCE_OVERWRITE) + force=app.FORCE_OVERWRITE, + preserve_pipes=True) diff --git a/lib/mrtrix3/run.py b/lib/mrtrix3/run.py index 3b70dda75b..75a05c2fab 100644 --- a/lib/mrtrix3/run.py +++ b/lib/mrtrix3/run.py @@ -79,9 +79,6 @@ def __init__(self): self._scratch_dir = None self.verbosity = 1 - # Ensures that temporary piped images are not deleted automatically by MRtrix3 binary commands - self.env['MRTRIX_PRESERVE_TMPFILE'] = 'yes' - # Acquire a unique index # This ensures that if command() is executed in parallel using different threads, they will # not interfere with one another; but terminate() will also have access to all relevant data @@ -233,7 +230,17 @@ def quote_nonpipe(item): show = kwargs.pop('show', True) mrconvert_keyval = kwargs.pop('mrconvert_keyval', None) force = kwargs.pop('force', False) - env = kwargs.pop('env', shared.env) + if 'env' in kwargs: + env = kwargs.pop('env') + if kwargs.pop('preserve_pipes', False): + env['MRTRIX_PRESERVE_TMPFILE'] = 'True' + elif 'preserve_pipes' in kwargs: + env = dict(shared.env) + kwargs.pop('preserve_pipes') + env['MRTRIX_PRESERVE_TMPFILE'] = 'True' + else: + # Reference rather than copying + env = shared.env if kwargs: raise TypeError('Unsupported keyword arguments passed to run.command(): ' + str(kwargs)) From 913ce0009f3372a6f4702ab2a650dfd4b1e53749 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 11 Aug 2023 08:46:03 +1000 Subject: [PATCH 22/75] mrtrix3.app: Terminate if piping image to stdout --- lib/mrtrix3/app.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 90cc42bc17..eb9a5ad27b 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -182,9 +182,20 @@ def _execute(module): #pylint: disable=unused-variable if hasattr(ARGS, 'config') and ARGS.config: for keyval in ARGS.config: CONFIG[keyval[0]] = keyval[1] + # ANSI settings may have been altered at the command-line setup_ansi() + # Check compatibility with command-line piping + # if _STDIN_IMAGES and sys.stdin.isatty(): + # sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Piped input images not available from stdin' + ANSI.clear + '\n') + # sys.stderr.flush() + # sys.exit(1) + if _STDOUT_IMAGES and sys.stdout.isatty(): + sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Cannot pipe output images as no command connected to stdout' + ANSI.clear + '\n') + sys.stderr.flush() + sys.exit(1) + if hasattr(ARGS, 'cont') and ARGS.cont: CONTINUE_OPTION = True SCRATCH_DIR = os.path.abspath(ARGS.cont[0]) @@ -1175,8 +1186,6 @@ def __call__(self, input_value): class ImageIn: def __call__(self, input_value): if input_value == '-': - if sys.stdin.isatty(): - raise argparse.ArgumentTypeError('Input piped image unavailable from stdin') input_value = sys.stdin.readline().strip() _STDIN_IMAGES.append(input_value) return input_value From 2e1efdbc3a56262e3e48095d061f55b224c6ca4c Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 11 Aug 2023 12:50:56 +1000 Subject: [PATCH 23/75] mrtrix3.app: Improvements to __print_full_usage__ behaviour --- docs/reference/commands/5ttgen.rst | 10 +- docs/reference/commands/dwi2mask.rst | 40 +-- docs/reference/commands/dwi2response.rst | 28 +- docs/reference/commands/dwibiascorrect.rst | 12 +- docs/reference/commands/dwicat.rst | 2 +- docs/reference/commands/dwifslpreproc.rst | 4 +- docs/reference/commands/dwigradcheck.rst | 4 +- docs/reference/commands/dwinormalise.rst | 8 +- docs/reference/commands/dwishellmath.rst | 4 +- docs/reference/commands/for_each.rst | 2 +- docs/reference/commands/labelsgmfix.rst | 2 +- docs/reference/commands/mask2glass.rst | 2 +- docs/reference/commands/mrtrix_cleanup.rst | 2 +- .../commands/population_template.rst | 2 +- docs/reference/commands/responsemean.rst | 2 +- lib/mrtrix3/app.py | 278 ++++++++++++------ 16 files changed, 243 insertions(+), 159 deletions(-) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index d24ebeb71d..13f1f3ab7f 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -41,7 +41,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -132,7 +132,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -227,7 +227,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -321,7 +321,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -415,7 +415,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index e9a35ce233..ca6b2541a5 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -31,7 +31,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -42,7 +42,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -140,7 +140,7 @@ Options specific to the 'afni_3dautomask' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -151,7 +151,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -231,7 +231,7 @@ Options specific to the "ants" algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -242,7 +242,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -343,7 +343,7 @@ Options specific to the "template" algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -354,7 +354,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -442,7 +442,7 @@ Options specific to the "consensus" algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -453,7 +453,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -539,7 +539,7 @@ Options specific to the 'fslbet' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -550,7 +550,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -625,7 +625,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -636,7 +636,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -713,7 +713,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -724,7 +724,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -804,7 +804,7 @@ Options specific to the 'mean' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -815,7 +815,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -902,7 +902,7 @@ Options specific to the 'trace' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -913,7 +913,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index 1d69c65c84..f9a6c2abd5 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -35,7 +35,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -57,7 +57,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -152,7 +152,7 @@ Options for the 'dhollander' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -174,7 +174,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -260,7 +260,7 @@ Options specific to the 'fa' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -282,7 +282,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -363,7 +363,7 @@ Options specific to the 'manual' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -385,7 +385,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -474,7 +474,7 @@ Options specific to the 'msmt_5tt' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -496,7 +496,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -580,7 +580,7 @@ Options specific to the 'tax' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -602,7 +602,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -688,7 +688,7 @@ Options specific to the 'tournier' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -710,7 +710,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwibiascorrect.rst b/docs/reference/commands/dwibiascorrect.rst index b9e6b3c231..ee537b2fe9 100644 --- a/docs/reference/commands/dwibiascorrect.rst +++ b/docs/reference/commands/dwibiascorrect.rst @@ -29,7 +29,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -47,7 +47,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -129,7 +129,7 @@ Options for ANTs N4BiasFieldCorrection command Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -147,7 +147,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -227,7 +227,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -245,7 +245,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwicat.rst b/docs/reference/commands/dwicat.rst index 1c1a354e55..8fea1afe72 100644 --- a/docs/reference/commands/dwicat.rst +++ b/docs/reference/commands/dwicat.rst @@ -35,7 +35,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwifslpreproc.rst b/docs/reference/commands/dwifslpreproc.rst index a9bf779205..ced787b6f5 100644 --- a/docs/reference/commands/dwifslpreproc.rst +++ b/docs/reference/commands/dwifslpreproc.rst @@ -80,7 +80,7 @@ Options for specifying the acquisition phase-encoding design; note that one of t Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -132,7 +132,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwigradcheck.rst b/docs/reference/commands/dwigradcheck.rst index 21dad6a498..399133dc8a 100644 --- a/docs/reference/commands/dwigradcheck.rst +++ b/docs/reference/commands/dwigradcheck.rst @@ -35,7 +35,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -53,7 +53,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwinormalise.rst b/docs/reference/commands/dwinormalise.rst index af3b195525..287a8ecff9 100644 --- a/docs/reference/commands/dwinormalise.rst +++ b/docs/reference/commands/dwinormalise.rst @@ -32,7 +32,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -121,7 +121,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -199,7 +199,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -210,7 +210,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwishellmath.rst b/docs/reference/commands/dwishellmath.rst index ed8830669c..5fe5826443 100644 --- a/docs/reference/commands/dwishellmath.rst +++ b/docs/reference/commands/dwishellmath.rst @@ -37,7 +37,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -48,7 +48,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/for_each.rst b/docs/reference/commands/for_each.rst index c2bf86d4aa..b830bb5afe 100644 --- a/docs/reference/commands/for_each.rst +++ b/docs/reference/commands/for_each.rst @@ -82,7 +82,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/labelsgmfix.rst b/docs/reference/commands/labelsgmfix.rst index 2151928a32..a6a4183029 100644 --- a/docs/reference/commands/labelsgmfix.rst +++ b/docs/reference/commands/labelsgmfix.rst @@ -34,7 +34,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/mask2glass.rst b/docs/reference/commands/mask2glass.rst index 6dbee1a510..9e00ee0bbd 100644 --- a/docs/reference/commands/mask2glass.rst +++ b/docs/reference/commands/mask2glass.rst @@ -41,7 +41,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/mrtrix_cleanup.rst b/docs/reference/commands/mrtrix_cleanup.rst index cafaefdf09..9d170eb1e9 100644 --- a/docs/reference/commands/mrtrix_cleanup.rst +++ b/docs/reference/commands/mrtrix_cleanup.rst @@ -40,7 +40,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/population_template.rst b/docs/reference/commands/population_template.rst index 43a4b7fdff..da217eda23 100644 --- a/docs/reference/commands/population_template.rst +++ b/docs/reference/commands/population_template.rst @@ -113,7 +113,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/responsemean.rst b/docs/reference/commands/responsemean.rst index 32e8436c7b..839843cf9f 100644 --- a/docs/reference/commands/responsemean.rst +++ b/docs/reference/commands/responsemean.rst @@ -39,7 +39,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index eb9a5ad27b..d2a44ce03f 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -592,6 +592,134 @@ def _get_message(self): class Parser(argparse.ArgumentParser): + + + # Various callable types for use as argparse argument types + class CustomTypeBase: + @staticmethod + def _typestring(): + assert False + + class Bool(CustomTypeBase): + def __call__(self, input_value): + processed_value = input_value.strip().lower() + if processed_value in ['true', 'yes']: + return True + if processed_value in ['false', 'no']: + return False + try: + processed_value = int(processed_value) + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as boolean value"') from exc + return bool(processed_value) + @staticmethod + def _typestring(): + return 'BOOL' + + class SequenceInt(CustomTypeBase): + def __call__(self, input_value): + try: + return [int(i) for i in input_value.split(',')] + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer sequence') from exc + @staticmethod + def _typestring(): + return 'ISEQ' + + class SequenceFloat(CustomTypeBase): + def __call__(self, input_value): + try: + return [float(i) for i in input_value.split(',')] + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point sequence') from exc + @staticmethod + def _typestring(): + return 'FSEQ' + + class DirectoryIn(CustomTypeBase): + def __call__(self, input_value): + if not os.path.exists(input_value): + raise argparse.ArgumentTypeError('Input directory "' + input_value + '" does not exist') + if not os.path.isdir(input_value): + raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a directory') + return input_value + @staticmethod + def _typestring(): + return 'DIRIN' + + class DirectoryOut(CustomTypeBase): + def __call__(self, input_value): + return input_value + @staticmethod + def _typestring(): + return 'DIROUT' + + class FileIn(CustomTypeBase): + def __call__(self, input_value): + if not os.path.exists(input_value): + raise argparse.ArgumentTypeError('Input file "' + input_value + '" does not exist') + if not os.path.isfile(input_value): + raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a file') + return input_value + def _typestring(): + return 'FILEIN' + + class FileOut(CustomTypeBase): + def __call__(self, input_value): + return input_value + @staticmethod + def _typestring(): + return 'FILEOUT' + + class ImageIn(CustomTypeBase): + def __call__(self, input_value): + if input_value == '-': + input_value = sys.stdin.readline().strip() + _STDIN_IMAGES.append(input_value) + return input_value + @staticmethod + def _typestring(): + return 'IMAGEIN' + + class ImageOut(CustomTypeBase): + def __call__(self, input_value): + if input_value == '-': + input_value = utils.name_temporary('mif') + _STDOUT_IMAGES.append(input_value) + return input_value + @staticmethod + def _typestring(): + return 'IMAGEOUT' + + class TracksIn(FileIn): + def __call__(self, input_value): + super().__call__(input_value) + if not input_value.endswith('.tck'): + raise argparse.ArgumentTypeError('Input tractogram file "' + input_value + '" is not a valid track file') + return input_value + @staticmethod + def _typestring(): + return 'TRACKSIN' + + class TracksOut(FileOut): + def __call__(self, input_value): + if not input_value.endswith('.tck'): + raise argparse.ArgumentTypeError('Output tractogram path "' + input_value + '" does not use the requisite ".tck" suffix') + return input_value + def _typestring(): + return 'TRACKSOUT' + + class Various(CustomTypeBase): + def __call__(self, input_value): + return input_value + @staticmethod + def _typestring(): + return 'VARIOUS' + + + + + # pylint: disable=protected-access def __init__(self, *args_in, **kwargs_in): self._author = None @@ -616,13 +744,13 @@ def __init__(self, *args_in, **kwargs_in): self.flag_mutually_exclusive_options( [ 'info', 'quiet', 'debug' ] ) standard_options.add_argument('-force', action='store_true', help='force overwrite of output files.') standard_options.add_argument('-nthreads', metavar='number', type=int, help='use this number of threads in multi-threaded applications (set to 0 to disable multi-threading).') - standard_options.add_argument('-config', action='append', metavar='key value', nargs=2, help='temporarily set the value of an MRtrix config file entry.') + standard_options.add_argument('-config', action='append', type=str, metavar=('key', 'value'), nargs=2, help='temporarily set the value of an MRtrix config file entry.') standard_options.add_argument('-help', action='store_true', help='display this information page and exit.') standard_options.add_argument('-version', action='store_true', help='display version information and exit.') script_options = self.add_argument_group('Additional standard options for Python scripts') script_options.add_argument('-nocleanup', action='store_true', help='do not delete intermediate files during script execution, and do not delete scratch directory at script completion.') - script_options.add_argument('-scratch', metavar='/path/to/scratch/', help='manually specify the path in which to generate the scratch directory.') - script_options.add_argument('-continue', nargs=2, dest='cont', metavar=('', ''), help='continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file.') + script_options.add_argument('-scratch', type=Parser.DirectoryOut, metavar='/path/to/scratch/', help='manually specify the path in which to generate the scratch directory.') + script_options.add_argument('-continue', nargs=2, dest='cont', metavar=('ScratchDir', 'LastFile'), help='continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file.') module_file = os.path.realpath (inspect.getsourcefile(inspect.stack()[-1][0])) self._is_project = os.path.abspath(os.path.join(os.path.dirname(module_file), os.pardir, 'lib', 'mrtrix3', 'app.py')) != os.path.abspath(__file__) try: @@ -741,6 +869,12 @@ def _check_mutex_options(self, args_in): sys.stderr.flush() sys.exit(1) + + + + + + def format_usage(self): argument_list = [ ] trailing_ellipsis = '' @@ -921,23 +1055,52 @@ def print_full_usage(self): self._subparsers._group_actions[0].choices[alg].print_full_usage() return self.error('Invalid subparser nominated') + + def arg2str(arg): + if arg.choices: + return 'CHOICE ' + ' '.join(arg.choices) + if isinstance(arg.type, int) or arg.type is int: + return 'INT' + if isinstance(arg.type, float) or arg.type is float: + return 'FLOAT' + if isinstance(arg.type, str) or arg.type is str or arg.type is None: + return 'TEXT' + if isinstance(arg.type, Parser.CustomTypeBase): + return type(arg.type)._typestring() + return arg.type._typestring() + + def allow_multiple(nargs): + return '1' if nargs in ('*', '+') else '0' + for arg in self._positionals._group_actions: - # This will need updating if any scripts allow mulitple argument inputs - sys.stdout.write('ARGUMENT ' + arg.dest + ' 0 0\n') + sys.stdout.write('ARGUMENT ' + arg.dest + ' 0 ' + allow_multiple(arg.nargs) + ' ' + arg2str(arg) + '\n') sys.stdout.write(arg.help + '\n') def print_group_options(group): for option in group._group_actions: - optional = '0' if option.required else '1' - allow_multiple = '1' if isinstance(option, argparse._AppendAction) else '0' - sys.stdout.write('OPTION ' + '/'.join(option.option_strings) + ' ' + optional + ' ' + allow_multiple + '\n') + sys.stdout.write('OPTION ' + + '/'.join(option.option_strings) + + ' ' + + ('0' if option.required else '1') + + ' ' + + allow_multiple(option.nargs) + + '\n') sys.stdout.write(option.help + '\n') - if option.metavar: - if isinstance(option.metavar, tuple): - for arg in option.metavar: - sys.stdout.write('ARGUMENT ' + arg + ' 0 0\n') - else: - sys.stdout.write('ARGUMENT ' + option.metavar + ' 0 0\n') + if option.metavar and isinstance(option.metavar, tuple): + assert len(option.metavar) == option.nargs + for arg in option.metavar: + sys.stdout.write('ARGUMENT ' + arg + ' 0 0 ' + arg2str(option) + '\n') + else: + multiple = allow_multiple(option.nargs) + nargs = 1 if multiple == '1' else (option.nargs if option.nargs is not None else 1) + for _ in range(0, nargs): + sys.stdout.write('ARGUMENT ' + + (option.metavar if option.metavar else '/'.join(option.option_strings)) + + ' 0 ' + + multiple + + ' ' + + arg2str(option) + + '\n') ungrouped_options = self._get_ungrouped_options() if ungrouped_options and ungrouped_options._group_actions: @@ -1131,92 +1294,13 @@ def _is_option_group(self, group): not group == self._positionals and \ group.title not in ( 'options', 'optional arguments' ) - # Various callable types for use as argparse argument types - class Bool: - def __call__(self, input_value): - processed_value = input_value.strip().lower() - if processed_value in ['true', 'yes']: - return True - if processed_value in ['false', 'no']: - return False - try: - processed_value = int(processed_value) - except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as boolean value"') from exc - return bool(processed_value) - - class SequenceInt: - def __call__(self, input_value): - try: - return [int(i) for i in input_value.split(',')] - except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer sequence') from exc - - class SequenceFloat: - def __call__(self, input_value): - try: - return [float(i) for i in input_value.split(',')] - except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point sequence') from exc - - class DirectoryIn: - def __call__(self, input_value): - if not os.path.exists(input_value): - raise argparse.ArgumentTypeError('Input directory "' + input_value + '" does not exist') - if not os.path.isdir(input_value): - raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a directory') - return input_value - - class DirectoryOut: - def __call__(self, input_value): - return input_value - - class FileIn: - def __call__(self, input_value): - if not os.path.exists(input_value): - raise argparse.ArgumentTypeError('Input file "' + input_value + '" does not exist') - if not os.path.isfile(input_value): - raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a file') - return input_value - - class FileOut: - def __call__(self, input_value): - return input_value - - class ImageIn: - def __call__(self, input_value): - if input_value == '-': - input_value = sys.stdin.readline().strip() - _STDIN_IMAGES.append(input_value) - return input_value - - class ImageOut: - def __call__(self, input_value): - if input_value == '-': - input_value = utils.name_temporary('mif') - _STDOUT_IMAGES.append(input_value) - return input_value - - class TracksIn(FileIn): - def __call__(self, input_value): - super().__call__(input_value) - if not input_value.endswith('.tck'): - raise argparse.ArgumentTypeError('Input tractogram file "' + input_value + '" is not a valid track file') - return input_value - - class TracksOut: - def __call__(self, input_value): - if not input_value.endswith('.tck'): - raise argparse.ArgumentTypeError('Output tractogram path "' + input_value + '" does not use the requisite ".tck" suffix') - return input_value - # Define functions for incorporating commonly-used command-line options / option groups def add_dwgrad_import_options(cmdline): #pylint: disable=unused-variable options = cmdline.add_argument_group('Options for importing the diffusion gradient table') - options.add_argument('-grad', help='Provide the diffusion gradient table in MRtrix format') - options.add_argument('-fslgrad', nargs=2, metavar=('bvecs', 'bvals'), help='Provide the diffusion gradient table in FSL bvecs/bvals format') + options.add_argument('-grad', type=Parser.FileIn, metavar='file', help='Provide the diffusion gradient table in MRtrix format') + options.add_argument('-fslgrad', type=Parser.FileIn, nargs=2, metavar=('bvecs', 'bvals'), help='Provide the diffusion gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'grad', 'fslgrad' ] ) def read_dwgrad_import_options(): #pylint: disable=unused-variable from mrtrix3 import path #pylint: disable=import-outside-toplevel @@ -1232,8 +1316,8 @@ def read_dwgrad_import_options(): #pylint: disable=unused-variable def add_dwgrad_export_options(cmdline): #pylint: disable=unused-variable options = cmdline.add_argument_group('Options for exporting the diffusion gradient table') - options.add_argument('-export_grad_mrtrix', metavar='grad', help='Export the final gradient table in MRtrix format') - options.add_argument('-export_grad_fsl', nargs=2, metavar=('bvecs', 'bvals'), help='Export the final gradient table in FSL bvecs/bvals format') + options.add_argument('-export_grad_mrtrix', type=Parser.FileOut, metavar='grad', help='Export the final gradient table in MRtrix format') + options.add_argument('-export_grad_fsl', type=Parser.FileOut, nargs=2, metavar=('bvecs', 'bvals'), help='Export the final gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'export_grad_mrtrix', 'export_grad_fsl' ] ) From ff56d40c73d3274d9284fe680c5b6e674dd20b83 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 11 Aug 2023 14:47:09 +1000 Subject: [PATCH 24/75] dwibiascorrect ants: Change command-line options Change to use underscore rather than dot point for better consistency with rest of MRtrix3 software. --- docs/reference/commands/dwibiascorrect.rst | 6 +++--- lib/mrtrix3/dwibiascorrect/ants.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/reference/commands/dwibiascorrect.rst b/docs/reference/commands/dwibiascorrect.rst index ee537b2fe9..cbbcb57082 100644 --- a/docs/reference/commands/dwibiascorrect.rst +++ b/docs/reference/commands/dwibiascorrect.rst @@ -120,11 +120,11 @@ Options Options for ANTs N4BiasFieldCorrection command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-ants.b [100,3]** N4BiasFieldCorrection option -b. [initial mesh resolution in mm, spline order] This value is optimised for human adult data and needs to be adjusted for rodent data. +- **-ants_b [100,3]** N4BiasFieldCorrection option -b: [initial mesh resolution in mm, spline order] This value is optimised for human adult data and needs to be adjusted for rodent data. -- **-ants.c [1000,0.0]** N4BiasFieldCorrection option -c. [numberOfIterations,convergenceThreshold] +- **-ants_c [1000,0.0]** N4BiasFieldCorrection option -c: [numberOfIterations,convergenceThreshold] -- **-ants.s 4** N4BiasFieldCorrection option -s. shrink-factor applied to spatial dimensions +- **-ants_s 4** N4BiasFieldCorrection option -s: shrink-factor applied to spatial dimensions Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/lib/mrtrix3/dwibiascorrect/ants.py b/lib/mrtrix3/dwibiascorrect/ants.py index 5dea100344..08928a88d6 100644 --- a/lib/mrtrix3/dwibiascorrect/ants.py +++ b/lib/mrtrix3/dwibiascorrect/ants.py @@ -21,8 +21,8 @@ OPT_N4_BIAS_FIELD_CORRECTION = { 's': ('4','shrink-factor applied to spatial dimensions'), - 'b':('[100,3]','[initial mesh resolution in mm, spline order] This value is optimised for human adult data and needs to be adjusted for rodent data.'), - 'c':('[1000,0.0]', '[numberOfIterations,convergenceThreshold]')} + 'b': ('[100,3]','[initial mesh resolution in mm, spline order] This value is optimised for human adult data and needs to be adjusted for rodent data.'), + 'c': ('[1000,0.0]', '[numberOfIterations,convergenceThreshold]')} @@ -33,7 +33,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Tustison, N.; Avants, B.; Cook, P.; Zheng, Y.; Egan, A.; Yushkevich, P. & Gee, J. N4ITK: Improved N3 Bias Correction. IEEE Transactions on Medical Imaging, 2010, 29, 1310-1320', is_external=True) ants_options = parser.add_argument_group('Options for ANTs N4BiasFieldCorrection command') for key in sorted(OPT_N4_BIAS_FIELD_CORRECTION): - ants_options.add_argument('-ants.'+key, metavar=OPT_N4_BIAS_FIELD_CORRECTION[key][0], help='N4BiasFieldCorrection option -%s. %s' % (key,OPT_N4_BIAS_FIELD_CORRECTION[key][1])) + ants_options.add_argument('-ants_'+key, metavar=OPT_N4_BIAS_FIELD_CORRECTION[key][0], help='N4BiasFieldCorrection option -%s: %s' % (key,OPT_N4_BIAS_FIELD_CORRECTION[key][1])) parser.add_argument('input', type=app.Parser.ImageIn(), help='The input image series to be corrected') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') @@ -54,8 +54,8 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Could not find ANTS program N4BiasFieldCorrection; please check installation') for key in sorted(OPT_N4_BIAS_FIELD_CORRECTION): - if hasattr(app.ARGS, 'ants.' + key): - val = getattr(app.ARGS, 'ants.' + key) + if hasattr(app.ARGS, 'ants_' + key): + val = getattr(app.ARGS, 'ants_' + key) if val is not None: OPT_N4_BIAS_FIELD_CORRECTION[key] = (val, 'user defined') ants_options = ' '.join(['-%s %s' %(k, v[0]) for k, v in OPT_N4_BIAS_FIELD_CORRECTION.items()]) From a84793857379bdc8fdb350f3d4f9e0fd1e3b8dbe Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 11 Aug 2023 14:48:05 +1000 Subject: [PATCH 25/75] mrtrix3.app: Fix __print_full_usage__ for subparser algorithms --- lib/mrtrix3/app.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index d2a44ce03f..70b07e3e82 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1037,6 +1037,12 @@ def print_group_options(group): sys.stdout.flush() def print_full_usage(self): + if self._subparsers and len(sys.argv) == 3: + for alg in self._subparsers._group_actions[0].choices: + if alg == sys.argv[1]: + self._subparsers._group_actions[0].choices[alg].print_full_usage() + return + self.error('Invalid subparser nominated') sys.stdout.write(self._synopsis + '\n') if self._description: if isinstance(self._description, list): @@ -1049,12 +1055,6 @@ def print_full_usage(self): if example[2]: sys.stdout.write('; ' + example[2]) sys.stdout.write('\n') - if self._subparsers and len(sys.argv) == 3: - for alg in self._subparsers._group_actions[0].choices: - if alg == sys.argv[1]: - self._subparsers._group_actions[0].choices[alg].print_full_usage() - return - self.error('Invalid subparser nominated') def arg2str(arg): if arg.choices: @@ -1095,7 +1095,7 @@ def print_group_options(group): nargs = 1 if multiple == '1' else (option.nargs if option.nargs is not None else 1) for _ in range(0, nargs): sys.stdout.write('ARGUMENT ' - + (option.metavar if option.metavar else '/'.join(option.option_strings)) + + (option.metavar if option.metavar else '/'.join(opt.lstrip('-') for opt in option.option_strings)) + ' 0 ' + multiple + ' ' From 94abafabfc22003b6b05f33e32ca242fb584219c Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 11 Aug 2023 14:51:10 +1000 Subject: [PATCH 26/75] Docs: Improved metavar usage in Python scripts --- docs/reference/commands/dwi2response.rst | 36 ++++++++++++------------ docs/reference/commands/dwinormalise.rst | 6 ++-- lib/mrtrix3/dwi2response/dhollander.py | 10 +++---- lib/mrtrix3/dwi2response/fa.py | 6 ++-- lib/mrtrix3/dwi2response/msmt_5tt.py | 6 ++-- lib/mrtrix3/dwi2response/tax.py | 6 ++-- lib/mrtrix3/dwi2response/tournier.py | 8 +++--- lib/mrtrix3/dwinormalise/group.py | 2 +- lib/mrtrix3/dwinormalise/individual.py | 4 +-- 9 files changed, 42 insertions(+), 42 deletions(-) diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index f9a6c2abd5..db2486f16d 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -137,15 +137,15 @@ Options Options for the 'dhollander' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-erode** Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3) +- **-erode passes** Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3) -- **-fa** FA threshold for crude WM versus GM-CSF separation. (default: 0.2) +- **-fa threshold** FA threshold for crude WM versus GM-CSF separation. (default: 0.2) -- **-sfwm** Final number of single-fibre WM voxels to select, as a percentage of refined WM. (default: 0.5 per cent) +- **-sfwm percentage** Final number of single-fibre WM voxels to select, as a percentage of refined WM. (default: 0.5 per cent) -- **-gm** Final number of GM voxels to select, as a percentage of refined GM. (default: 2 per cent) +- **-gm percentage** Final number of GM voxels to select, as a percentage of refined GM. (default: 2 per cent) -- **-csf** Final number of CSF voxels to select, as a percentage of refined CSF. (default: 10 per cent) +- **-csf percentage** Final number of CSF voxels to select, as a percentage of refined CSF. (default: 10 per cent) - **-wm_algo algorithm** Use external dwi2response algorithm for WM single-fibre voxel selection (options: fa, tax, tournier) (default: built-in Dhollander 2019) @@ -251,11 +251,11 @@ Options Options specific to the 'fa' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-erode** Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually) +- **-erode passes** Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually) -- **-number** The number of highest-FA voxels to use +- **-number voxels** The number of highest-FA voxels to use -- **-threshold** Apply a hard FA threshold, rather than selecting the top voxels +- **-threshold value** Apply a hard FA threshold, rather than selecting the top voxels Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -463,13 +463,13 @@ Options specific to the 'msmt_5tt' algorithm - **-dirs image** Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise) -- **-fa** Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2) +- **-fa value** Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2) -- **-pvf** Partial volume fraction threshold for tissue voxel selection (default: 0.95) +- **-pvf fraction** Partial volume fraction threshold for tissue voxel selection (default: 0.95) - **-wm_algo algorithm** dwi2response algorithm to use for WM single-fibre voxel selection (options: fa, tax, tournier; default: tournier) -- **-sfwm_fa_threshold** Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, which is passed to the -threshold option of the fa algorithm (warning: overrides -wm_algo option) +- **-sfwm_fa_threshold value** Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, which is passed to the -threshold option of the fa algorithm (warning: overrides -wm_algo option) Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -571,11 +571,11 @@ Options Options specific to the 'tax' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-peak_ratio** Second-to-first-peak amplitude ratio threshold +- **-peak_ratio value** Second-to-first-peak amplitude ratio threshold -- **-max_iters** Maximum number of iterations +- **-max_iters iterations** Maximum number of iterations -- **-convergence** Percentile change in any RF coefficient required to continue iterating +- **-convergence percentage** Percentile change in any RF coefficient required to continue iterating Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -677,13 +677,13 @@ Options Options specific to the 'tournier' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-number** Number of single-fibre voxels to use when calculating response function +- **-number voxels** Number of single-fibre voxels to use when calculating response function -- **-iter_voxels** Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number) +- **-iter_voxels voxels** Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number) -- **-dilate** Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration +- **-dilate passes** Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration -- **-max_iters** Maximum number of iterations +- **-max_iters iterations** Maximum number of iterations Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwinormalise.rst b/docs/reference/commands/dwinormalise.rst index 287a8ecff9..0df71f38e5 100644 --- a/docs/reference/commands/dwinormalise.rst +++ b/docs/reference/commands/dwinormalise.rst @@ -112,7 +112,7 @@ All input DWI files must contain an embedded diffusion gradient table; for this Options ------- -- **-fa_threshold** The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4) +- **-fa_threshold value** The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -192,9 +192,9 @@ Usage Options ------- -- **-intensity** Normalise the b=0 signal to a specified value (Default: 1000) +- **-intensity value** Normalise the b=0 signal to a specified value (Default: 1000) -- **-percentile** Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value +- **-percentile value** Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index e607186791..fa71259bf5 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -36,11 +36,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('out_gm', type=app.Parser.FileOut(), help='Output GM response function text file') parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response function text file') options = parser.add_argument_group('Options for the \'dhollander\' algorithm') - options.add_argument('-erode', type=int, default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') - options.add_argument('-fa', type=float, default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') - options.add_argument('-sfwm', type=float, default=0.5, help='Final number of single-fibre WM voxels to select, as a percentage of refined WM. (default: 0.5 per cent)') - options.add_argument('-gm', type=float, default=2.0, help='Final number of GM voxels to select, as a percentage of refined GM. (default: 2 per cent)') - options.add_argument('-csf', type=float, default=10.0, help='Final number of CSF voxels to select, as a percentage of refined CSF. (default: 10 per cent)') + options.add_argument('-erode', type=int, metavar='passes', default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') + options.add_argument('-fa', type=float, metavar='threshold', default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') + options.add_argument('-sfwm', type=float, metavar='percentage', default=0.5, help='Final number of single-fibre WM voxels to select, as a percentage of refined WM. (default: 0.5 per cent)') + options.add_argument('-gm', type=float, metavar='percentage', default=2.0, help='Final number of GM voxels to select, as a percentage of refined GM. (default: 2 per cent)') + options.add_argument('-csf', type=float, metavar='percentage', default=10.0, help='Final number of CSF voxels to select, as a percentage of refined CSF. (default: 10 per cent)') options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, help='Use external dwi2response algorithm for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + ') (default: built-in Dhollander 2019)') diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index 52dab35253..d1ec976e1a 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -27,9 +27,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'fa\' algorithm') - options.add_argument('-erode', type=int, default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') - options.add_argument('-number', type=int, default=300, help='The number of highest-FA voxels to use') - options.add_argument('-threshold', type=float, help='Apply a hard FA threshold, rather than selecting the top voxels') + options.add_argument('-erode', type=int, metavar='passes', default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') + options.add_argument('-number', type=int, metavar='voxels', default=300, help='The number of highest-FA voxels to use') + options.add_argument('-threshold', type=float, metavar='value', help='Apply a hard FA threshold, rather than selecting the top voxels') parser.flag_mutually_exclusive_options( [ 'number', 'threshold' ] ) diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index 7d3068385b..895cf21ff5 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -35,10 +35,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response text file') options = parser.add_argument_group('Options specific to the \'msmt_5tt\' algorithm') options.add_argument('-dirs', type=app.Parser.ImageIn(), metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') - options.add_argument('-fa', type=float, default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') - options.add_argument('-pvf', type=float, default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') + options.add_argument('-fa', type=float, metavar='value', default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') + options.add_argument('-pvf', type=float, metavar='fraction', default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, default='tournier', help='dwi2response algorithm to use for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + '; default: tournier)') - options.add_argument('-sfwm_fa_threshold', type=float, help='Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, which is passed to the -threshold option of the fa algorithm (warning: overrides -wm_algo option)') + options.add_argument('-sfwm_fa_threshold', type=float, metavar='value', help='Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, which is passed to the -threshold option of the fa algorithm (warning: overrides -wm_algo option)') diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index 2dc4d7b60e..294555c4c5 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -27,9 +27,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tax\' algorithm') - options.add_argument('-peak_ratio', type=float, default=0.1, help='Second-to-first-peak amplitude ratio threshold') - options.add_argument('-max_iters', type=int, default=20, help='Maximum number of iterations') - options.add_argument('-convergence', type=float, default=0.5, help='Percentile change in any RF coefficient required to continue iterating') + options.add_argument('-peak_ratio', type=float, metavar='value', default=0.1, help='Second-to-first-peak amplitude ratio threshold') + options.add_argument('-max_iters', type=int, metavar='iterations', default=20, help='Maximum number of iterations') + options.add_argument('-convergence', type=float, metavar='percentage', default=0.5, help='Percentile change in any RF coefficient required to continue iterating') diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index 097e453539..6411bf073d 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -27,10 +27,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tournier\' algorithm') - options.add_argument('-number', type=int, default=300, help='Number of single-fibre voxels to use when calculating response function') - options.add_argument('-iter_voxels', type=int, default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') - options.add_argument('-dilate', type=int, default=1, help='Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration') - options.add_argument('-max_iters', type=int, default=10, help='Maximum number of iterations') + options.add_argument('-number', type=int, metavar='voxels', default=300, help='Number of single-fibre voxels to use when calculating response function') + options.add_argument('-iter_voxels', type=int, metavar='voxels', default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') + options.add_argument('-dilate', type=int, metavar='passes', default=1, help='Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration') + options.add_argument('-max_iters', type=int, metavar='iterations', default=10, help='Maximum number of iterations') diff --git a/lib/mrtrix3/dwinormalise/group.py b/lib/mrtrix3/dwinormalise/group.py index b76af7887c..6c824211b9 100644 --- a/lib/mrtrix3/dwinormalise/group.py +++ b/lib/mrtrix3/dwinormalise/group.py @@ -30,7 +30,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('output_dir', type=app.Parser.DirectoryOut(), help='The output directory containing all of the intensity normalised DWI images') parser.add_argument('fa_template', type=app.Parser.ImageOut(), help='The output population-specific FA template, which is thresholded to estimate a white matter mask') parser.add_argument('wm_mask', type=app.Parser.ImageOut(), help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') - parser.add_argument('-fa_threshold', default='0.4', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4)') + parser.add_argument('-fa_threshold', default='0.4', metavar='value', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4)') diff --git a/lib/mrtrix3/dwinormalise/individual.py b/lib/mrtrix3/dwinormalise/individual.py index 9ed0dc11b5..5f32bc9f7c 100644 --- a/lib/mrtrix3/dwinormalise/individual.py +++ b/lib/mrtrix3/dwinormalise/individual.py @@ -29,8 +29,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input_dwi', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('input_mask', type=app.Parser.ImageIn(), help='The mask within which a reference b=0 intensity will be sampled') parser.add_argument('output_dwi', type=app.Parser.ImageOut(), help='The output intensity-normalised DWI series') - parser.add_argument('-intensity', type=float, default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value (Default: ' + str(DEFAULT_TARGET_INTENSITY) + ')') - parser.add_argument('-percentile', type=int, help='Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value') + parser.add_argument('-intensity', type=float, metavar='value', default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value (Default: ' + str(DEFAULT_TARGET_INTENSITY) + ')') + parser.add_argument('-percentile', type=int, metavar='value', help='Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value') app.add_dwgrad_import_options(parser) From 1a9759ebbf16b2d1341dacfbc40afb732aeac260 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 11 Aug 2023 15:34:58 +1000 Subject: [PATCH 27/75] dwi2mask consensus: Generalise -algorithm interface Allow user input to -algorithm command-line option to be either a space-separated list (which involves argparse consuming multiple arguments), or a single string as a comma-separated list (which is more consistent with the rest of MRtrix3). In addition, do not prevent execution due to the absence of template image information if there is no algorithm included in the list to execute that requires the use of such image data. --- docs/reference/commands/dwi2mask.rst | 2 +- lib/mrtrix3/dwi2mask/consensus.py | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index 00d00ed0fe..5600dc884a 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -431,7 +431,7 @@ Options Options specific to the "consensus" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-algorithms** Provide a list of dwi2mask algorithms that are to be utilised +- **-algorithms** Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised - **-masks image** Export a 4D image containing the individual algorithm masks diff --git a/lib/mrtrix3/dwi2mask/consensus.py b/lib/mrtrix3/dwi2mask/consensus.py index fbdc82fab0..2d6889c0b6 100644 --- a/lib/mrtrix3/dwi2mask/consensus.py +++ b/lib/mrtrix3/dwi2mask/consensus.py @@ -13,6 +13,7 @@ # # For more details, see http://www.mrtrix.org/. +import os from mrtrix3 import CONFIG, MRtrixError from mrtrix3 import algorithm, app, path, run @@ -25,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the "consensus" algorithm') - options.add_argument('-algorithms', nargs='+', help='Provide a list of dwi2mask algorithms that are to be utilised') + options.add_argument('-algorithms', nargs='+', help='Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised') options.add_argument('-masks', type=app.Parser.ImageOut(), metavar='image', help='Export a 4D image containing the individual algorithm masks') options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') options.add_argument('-threshold', type=float, default=DEFAULT_THRESHOLD, help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: ' + str(DEFAULT_THRESHOLD) + ')') @@ -43,9 +44,6 @@ def get_inputs(): #pylint: disable=unused-variable + ' -strides +1,+2,+3') run.command('mrconvert ' + CONFIG['Dwi2maskTemplateMask'] + ' ' + path.to_scratch('template_mask.nii') + ' -strides +1,+2,+3 -datatype uint8') - else: - raise MRtrixError('No template image information available from ' - 'either command-line or MRtrix configuration file(s)') @@ -66,15 +64,18 @@ def execute(): #pylint: disable=unused-variable app.debug(str(algorithm_list)) if app.ARGS.algorithms: - if 'consensus' in app.ARGS.algorithms: + user_algorithms = app.ARGS.algorithms + if len(user_algorithms) == 1: + user_algorithms = user_algorithms[0].split(',') + if 'consensus' in user_algorithms: raise MRtrixError('Cannot provide "consensus" in list of dwi2mask algorithms to utilise') - invalid_algs = [entry for entry in app.ARGS.algorithms if entry not in algorithm_list] + invalid_algs = [entry for entry in user_algorithms if entry not in algorithm_list] if invalid_algs: raise MRtrixError('Requested dwi2mask algorithm' + ('s' if len(invalid_algs) > 1 else '') + ' not available: ' + str(invalid_algs)) - algorithm_list = app.ARGS.algorithms + algorithm_list = user_algorithms # For "template" algorithm, can run twice with two different softwares # Ideally this would be determined based on the help page, @@ -86,6 +87,11 @@ def execute(): #pylint: disable=unused-variable algorithm_list.append('b02template -software fsl') app.debug(str(algorithm_list)) + if any(any(item in alg for item in ('ants', 'b02template')) for alg in algorithm_list) \ + and not os.path.isfile('template_image.nii'): + raise MRtrixError('Cannot include within consensus algorithms that necessitate use of a template image ' + 'if no template image is provided via command-line or configuration file') + mask_list = [] for alg in algorithm_list: alg_string = alg.replace(' -software ', '_') @@ -111,7 +117,7 @@ def execute(): #pylint: disable=unused-variable if not mask_list: raise MRtrixError('No dwi2mask algorithms were successful; cannot generate mask') if len(mask_list) == 1: - app.warn('Only one dwi2mask algorithm was successful; output mask will be this result and not a consensus') + app.warn('Only one dwi2mask algorithm was successful; output mask will be this result and not a "consensus"') if app.ARGS.masks: run.command('mrconvert ' + mask_list[0] + ' ' + path.from_user(app.ARGS.masks), mrconvert_keyval=path.from_user(app.ARGS.input, False), From a22382f88559f12511fac9376e93f858df86d436 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 11 Aug 2023 15:40:48 +1000 Subject: [PATCH 28/75] dwi2mask legacy: Make use of Python command image piping --- lib/mrtrix3/dwi2mask/legacy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/mrtrix3/dwi2mask/legacy.py b/lib/mrtrix3/dwi2mask/legacy.py index 113e51c4dc..c417a1cc82 100644 --- a/lib/mrtrix3/dwi2mask/legacy.py +++ b/lib/mrtrix3/dwi2mask/legacy.py @@ -46,10 +46,9 @@ def needs_mean_bzero(): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable - run.command('mrcalc input.mif 0 -max input_nonneg.mif') - run.command('dwishellmath input_nonneg.mif mean trace.mif') - app.cleanup('input_nonneg.mif') - run.command('mrthreshold trace.mif - -comparison gt | ' + run.command('mrcalc input.mif 0 -max - | ' + 'dwishellmath - mean - | ' + 'mrthreshold - - -comparison gt | ' 'mrmath - max -axis 3 - | ' 'maskfilter - median - | ' 'maskfilter - connect -largest - | ' From 7f27e89c70b94db1eaf87b6be667748336136845 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 30 Jan 2024 11:10:32 +1100 Subject: [PATCH 29/75] Python API: Ranged integers and floats - New callable types for argparse that enforce minimal and maximal values at time of command-line parsing. This is also done in such a way that these bounds can be interrogated outside of the parsing process itself; eg. such that they can appear in the output of __print_full_usage__. - Add typing to command-line arguments and options for new scripts and algorithms added in #2604. - For dwi2mask synthstrip, -border option changed from integer to float. - dwi2response tax and dwi2response tournier: permit number of iterations to be 0, in which case processing continues until convergence is achieved. - dwinormalise group: Change -fa_threshold from string to float. --- bin/dwibiasnormmask | 8 ++--- bin/dwifslpreproc | 2 +- bin/dwigradcheck | 2 +- bin/mask2glass | 6 ++-- bin/population_template | 6 ++-- lib/mrtrix3/app.py | 46 +++++++++++++++++++++++--- lib/mrtrix3/dwi2mask/3dautomask.py | 12 +++---- lib/mrtrix3/dwi2mask/consensus.py | 2 +- lib/mrtrix3/dwi2mask/fslbet.py | 6 ++-- lib/mrtrix3/dwi2mask/legacy.py | 2 +- lib/mrtrix3/dwi2mask/mean.py | 2 +- lib/mrtrix3/dwi2mask/mtnorm.py | 8 +++-- lib/mrtrix3/dwi2mask/synthstrip.py | 4 +-- lib/mrtrix3/dwi2mask/trace.py | 2 +- lib/mrtrix3/dwi2response/dhollander.py | 10 +++--- lib/mrtrix3/dwi2response/fa.py | 6 ++-- lib/mrtrix3/dwi2response/msmt_5tt.py | 6 ++-- lib/mrtrix3/dwi2response/tax.py | 8 ++--- lib/mrtrix3/dwi2response/tournier.py | 13 +++----- lib/mrtrix3/dwibiascorrect/mtnorm.py | 5 +-- lib/mrtrix3/dwinormalise/group.py | 6 ++-- lib/mrtrix3/dwinormalise/manual.py | 6 ++-- lib/mrtrix3/dwinormalise/mtnorm.py | 9 +++-- 23 files changed, 108 insertions(+), 69 deletions(-) diff --git a/bin/dwibiasnormmask b/bin/dwibiasnormmask index d3a18e6dcd..e71c42144e 100755 --- a/bin/dwibiasnormmask +++ b/bin/dwibiasnormmask @@ -94,21 +94,21 @@ def usage(cmdline): #pylint: disable=unused-variable help='Write the scaling factor applied to the DWI series to a text file') output_options.add_argument('-output_tissuesum', metavar='image', help='Export the tissue sum image that was used to generate the final mask') - output_options.add_argument('-reference', type=float, metavar='value', default=REFERENCE_INTENSITY, + output_options.add_argument('-reference', type=app.Parser.Float(0.0), metavar='value', default=REFERENCE_INTENSITY, help='Set the target CSF b=0 intensity in the output DWI series (default: ' + str(REFERENCE_INTENSITY) + ')') internal_options = cmdline.add_argument_group('Options relevant to the internal optimisation procedure') - internal_options.add_argument('-dice', type=float, default=DICE_COEFF_DEFAULT, metavar='value', + internal_options.add_argument('-dice', type=app.Parser.Float(0.0, 1.0), default=DICE_COEFF_DEFAULT, metavar='value', help='Set the Dice coefficient threshold for similarity of masks between sequential iterations that will ' 'result in termination due to convergence; default = ' + str(DICE_COEFF_DEFAULT)) internal_options.add_argument('-init_mask', metavar='image', help='Provide an initial mask for the first iteration of the algorithm ' '(if not provided, the default dwi2mask algorithm will be used)') - internal_options.add_argument('-max_iters', type=int, default=DWIBIASCORRECT_MAX_ITERS, metavar='count', + internal_options.add_argument('-max_iters', type=app.Parser.Int(0), default=DWIBIASCORRECT_MAX_ITERS, metavar='count', help='The maximum number of iterations (see Description); default is ' + str(DWIBIASCORRECT_MAX_ITERS) + '; ' 'set to 0 to proceed until convergence') internal_options.add_argument('-mask_algo', choices=MASK_ALGOS, metavar='algorithm', help='The algorithm to use for mask estimation, potentially based on the ODF sum image (see Description); default: ' + MASK_ALGO_DEFAULT) - internal_options.add_argument('-lmax', metavar='values', + internal_options.add_argument('-lmax', metavar='values', type=app.Parser.SequenceInt, help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') app.add_dwgrad_import_options(cmdline) diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index 80155e5846..baa83fe169 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -55,7 +55,7 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_argument('-json_import', type=app.Parser.FileIn(), metavar='file', help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') pe_options.add_argument('-pe_dir', metavar='PE', help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') - pe_options.add_argument('-readout_time', metavar='time', type=float, help='Manually specify the total readout time of the input series (in seconds)') + pe_options.add_argument('-readout_time', metavar='time', type=app.Parser.Float(0.0), help='Manually specify the total readout time of the input series (in seconds)') distcorr_options = cmdline.add_argument_group('Options for achieving correction of susceptibility distortions') distcorr_options.add_argument('-se_epi', type=app.Parser.ImageIn(), metavar='image', help='Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series)') distcorr_options.add_argument('-align_seepi', action='store_true', help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation, and the DWIs (more information in Description section)') diff --git a/bin/dwigradcheck b/bin/dwigradcheck index 362081e0d3..f11ad6c1cd 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -31,7 +31,7 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Jeurissen, B.; Leemans, A.; Sijbers, J. Automated correction of improperly rotated diffusion gradient orientations in diffusion weighted MRI. Medical Image Analysis, 2014, 18(7), 953-962') cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series to be checked') cmdline.add_argument('-mask', metavar='image', type=app.Parser.ImageIn(), help='Provide a mask image within which to seed & constrain tracking') - cmdline.add_argument('-number', type=int, default=10000, help='Set the number of tracks to generate for each test') + cmdline.add_argument('-number', type=app.Parser.Int(1), default=10000, help='Set the number of tracks to generate for each test') app.add_dwgrad_export_options(cmdline) app.add_dwgrad_import_options(cmdline) diff --git a/bin/mask2glass b/bin/mask2glass index 969f0677af..56efdd8c9c 100755 --- a/bin/mask2glass +++ b/bin/mask2glass @@ -27,9 +27,9 @@ def usage(cmdline): #pylint: disable=unused-variable 'than if a binary template mask were to be used as input.') cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input mask image') cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output glass brain image') - cmdline.add_argument('-dilate', type=int, default=2, help='Provide number of passes for dilation step; default = 2') - cmdline.add_argument('-scale', type=float, default=2.0, help='Provide resolution upscaling value; default = 2.0') - cmdline.add_argument('-smooth', type=float, default=1.0, help='Provide standard deviation of smoothing (in mm); default = 1.0') + cmdline.add_argument('-dilate', type=app.Parser.Int(0), default=2, help='Provide number of passes for dilation step; default = 2') + cmdline.add_argument('-scale', type=app.Parser.Float(0.0), default=2.0, help='Provide resolution upscaling value; default = 2.0') + cmdline.add_argument('-smooth', type=app.Parser.Float(0.0), default=1.0, help='Provide standard deviation of smoothing (in mm); default = 1.0') def execute(): #pylint: disable=unused-variable diff --git a/bin/population_template b/bin/population_template index 9cb3300b92..8775ef5f67 100755 --- a/bin/population_template +++ b/bin/population_template @@ -79,9 +79,9 @@ def usage(cmdline): #pylint: disable=unused-variable nloptions.add_argument('-nl_scale', type=app.Parser().SequenceFloat(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) nloptions.add_argument('-nl_lmax', type=app.Parser().SequenceInt(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) nloptions.add_argument('-nl_niter', type=app.Parser().SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) - nloptions.add_argument('-nl_update_smooth', type=float, default=DEFAULT_NL_UPDATE_SMOOTH, help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_UPDATE_SMOOTH) + ' x voxel_size)') - nloptions.add_argument('-nl_disp_smooth', type=float, default=DEFAULT_NL_DISP_SMOOTH, help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_DISP_SMOOTH) + ' x voxel_size)') - nloptions.add_argument('-nl_grad_step', type=float, default=DEFAULT_NL_GRAD_STEP, help='The gradient step size for non-linear registration (Default: ' + str(DEFAULT_NL_GRAD_STEP) + ')') + nloptions.add_argument('-nl_update_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_UPDATE_SMOOTH, help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_UPDATE_SMOOTH) + ' x voxel_size)') + nloptions.add_argument('-nl_disp_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_DISP_SMOOTH, help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_DISP_SMOOTH) + ' x voxel_size)') + nloptions.add_argument('-nl_grad_step', type=app.Parser.Float(0.0), default=DEFAULT_NL_GRAD_STEP, help='The gradient step size for non-linear registration (Default: ' + str(DEFAULT_NL_GRAD_STEP) + ')') options = cmdline.add_argument_group('Input, output and general options') options.add_argument('-type', choices=REGISTRATION_MODES, help='Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"' + x + '"' for x in REGISTRATION_MODES if "_" in x), default='rigid_affine_nonlinear') diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 8d71b2ab5d..b3746b54c5 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -616,6 +616,46 @@ def __call__(self, input_value): def _typestring(): return 'BOOL' + def Int(min_value=None, max_value=None): + assert min_value is None or isinstance(min_value, int) + assert max_value is None or isinstance(max_value, int) + assert min_value is None or max_value is None or max_value >= min_value + class Checker(Parser.CustomTypeBase): + def __call__(self, input_value): + try: + value = int(input_value) + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer value') from exc + if min_value is not None and value < min_value: + raise argparse.ArgumentTypeError('Input value "' + input_value + ' less than minimum permissible value ' + str(min_value)) + if max_value is not None and value > max_value: + raise argparse.ArgumentTypeError('Input value "' + input_value + ' greater than maximum permissible value ' + str(max_value)) + return value + @staticmethod + def _typestring(): + return 'INT ' + ('-9223372036854775808' if min_value is None else str(min_value)) + ' ' + ('9223372036854775807' if max_value is None else str(max_value)) + return Checker + + def Float(min_value=None, max_value=None): + assert min_value is None or isinstance(min_value, float) + assert max_value is None or isinstance(max_value, float) + assert min_value is None or max_value is None or max_value >= min_value + class Checker(Parser.CustomTypeBase): + def __call__(self, input_value): + try: + value = float(input_value) + except ValueError as exc: + raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point value') from exc + if min_value is not None and value < min_value: + raise argparse.ArgumentTypeError('Input value "' + input_value + ' less than minimum permissible value ' + str(min_value)) + if max_value is not None and value > max_value: + raise argparse.ArgumentTypeError('Input value "' + input_value + ' greater than maximum permissible value ' + str(max_value)) + return value + @staticmethod + def _typestring(): + return 'FLOAT ' + ('-inf' if min_value is None else str(min_value)) + ' ' + ('inf' if max_value is None else str(max_value)) + return Checker + class SequenceInt(CustomTypeBase): def __call__(self, input_value): try: @@ -743,7 +783,7 @@ def __init__(self, *args_in, **kwargs_in): standard_options.add_argument('-debug', action='store_true', help='display debugging messages.') self.flag_mutually_exclusive_options( [ 'info', 'quiet', 'debug' ] ) standard_options.add_argument('-force', action='store_true', help='force overwrite of output files.') - standard_options.add_argument('-nthreads', metavar='number', type=int, help='use this number of threads in multi-threaded applications (set to 0 to disable multi-threading).') + standard_options.add_argument('-nthreads', metavar='number', type=Parser.Int(0), help='use this number of threads in multi-threaded applications (set to 0 to disable multi-threading).') standard_options.add_argument('-config', action='append', type=str, metavar=('key', 'value'), nargs=2, help='temporarily set the value of an MRtrix config file entry.') standard_options.add_argument('-help', action='store_true', help='display this information page and exit.') standard_options.add_argument('-version', action='store_true', help='display version information and exit.') @@ -1059,10 +1099,6 @@ def print_full_usage(self): def arg2str(arg): if arg.choices: return 'CHOICE ' + ' '.join(arg.choices) - if isinstance(arg.type, int) or arg.type is int: - return 'INT' - if isinstance(arg.type, float) or arg.type is float: - return 'FLOAT' if isinstance(arg.type, str) or arg.type is str or arg.type is None: return 'TEXT' if isinstance(arg.type, Parser.CustomTypeBase): diff --git a/lib/mrtrix3/dwi2mask/3dautomask.py b/lib/mrtrix3/dwi2mask/3dautomask.py index e4f7bcc3bd..a42b39ed22 100644 --- a/lib/mrtrix3/dwi2mask/3dautomask.py +++ b/lib/mrtrix3/dwi2mask/3dautomask.py @@ -28,14 +28,14 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'afni_3dautomask\' algorithm') - options.add_argument('-clfrac', type=float, help='Set the \'clip level fraction\', must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger.') + options.add_argument('-clfrac', type=app.Parser.Float(0.1, 0.9), help='Set the \'clip level fraction\', must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger.') options.add_argument('-nograd', action='store_true', help='The program uses a \'gradual\' clip level by default. Add this option to use a fixed clip level.') - options.add_argument('-peels', type=float, help='Peel (erode) the mask n times, then unpeel (dilate).') - options.add_argument('-nbhrs', type=int, help='Define the number of neighbors needed for a voxel NOT to be eroded. It should be between 6 and 26.') + options.add_argument('-peels', type=app.Parser.Int(0), help='Peel (erode) the mask n times, then unpeel (dilate).') + options.add_argument('-nbhrs', type=app.Parser.Int(6, 26), help='Define the number of neighbors needed for a voxel NOT to be eroded. It should be between 6 and 26.') options.add_argument('-eclip', action='store_true', help='After creating the mask, remove exterior voxels below the clip threshold.') - options.add_argument('-SI', type=float, help='After creating the mask, find the most superior voxel, then zero out everything more than SI millimeters inferior to that. 130 seems to be decent (i.e., for Homo Sapiens brains).') - options.add_argument('-dilate', type=int, help='Dilate the mask outwards n times') - options.add_argument('-erode', type=int, help='Erode the mask outwards n times') + options.add_argument('-SI', type=app.Parser.Float(0.0), help='After creating the mask, find the most superior voxel, then zero out everything more than SI millimeters inferior to that. 130 seems to be decent (i.e., for Homo Sapiens brains).') + options.add_argument('-dilate', type=app.Parser.Int(0), help='Dilate the mask outwards n times') + options.add_argument('-erode', type=app.Parser.Int(0), help='Erode the mask outwards n times') options.add_argument('-NN1', action='store_true', help='Erode and dilate based on mask faces') options.add_argument('-NN2', action='store_true', help='Erode and dilate based on mask edges') diff --git a/lib/mrtrix3/dwi2mask/consensus.py b/lib/mrtrix3/dwi2mask/consensus.py index 2d6889c0b6..5997117f61 100644 --- a/lib/mrtrix3/dwi2mask/consensus.py +++ b/lib/mrtrix3/dwi2mask/consensus.py @@ -29,7 +29,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options.add_argument('-algorithms', nargs='+', help='Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised') options.add_argument('-masks', type=app.Parser.ImageOut(), metavar='image', help='Export a 4D image containing the individual algorithm masks') options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') - options.add_argument('-threshold', type=float, default=DEFAULT_THRESHOLD, help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: ' + str(DEFAULT_THRESHOLD) + ')') + options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), default=DEFAULT_THRESHOLD, help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: ' + str(DEFAULT_THRESHOLD) + ')') diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index fc365fed05..e439e71c54 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -27,10 +27,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the \'fslbet\' algorithm') - options.add_argument('-bet_f', type=float, help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') - options.add_argument('-bet_g', type=float, help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') + options.add_argument('-bet_f', type=app.Parser.Float(0.0, 1.0), help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') + options.add_argument('-bet_g', type=app.Parser.Float(-1.0, 1.0), help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') options.add_argument('-bet_c', type=app.Parser.SequenceFloat(), metavar='i,j,k', help='Centre-of-gravity (voxels not mm) of initial mesh surface') - options.add_argument('-bet_r', type=float, help='Head radius (mm not voxels); initial surface sphere is set to half of this') + options.add_argument('-bet_r', type=app.Parser.Float(0.0), help='Head radius (mm not voxels); initial surface sphere is set to half of this') options.add_argument('-rescale', action='store_true', help='Rescale voxel size provided to BET to 1mm isotropic; can improve results for rodent data') diff --git a/lib/mrtrix3/dwi2mask/legacy.py b/lib/mrtrix3/dwi2mask/legacy.py index c417a1cc82..324d7230fe 100644 --- a/lib/mrtrix3/dwi2mask/legacy.py +++ b/lib/mrtrix3/dwi2mask/legacy.py @@ -26,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') parser.add_argument('-clean_scale', - type=int, + type=app.Parser.Int(0), default=DEFAULT_CLEAN_SCALE, help='the maximum scale used to cut bridges. A certain maximum scale cuts ' 'bridges up to a width (in voxels) of 2x the provided scale. Setting ' diff --git a/lib/mrtrix3/dwi2mask/mean.py b/lib/mrtrix3/dwi2mask/mean.py index 03d86f290c..da2342f82a 100644 --- a/lib/mrtrix3/dwi2mask/mean.py +++ b/lib/mrtrix3/dwi2mask/mean.py @@ -26,7 +26,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the \'mean\' algorithm') options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='Comma separated list of shells to be included in the volume averaging') options.add_argument('-clean_scale', - type=int, + type=app.Parser.Int(0), default=DEFAULT_CLEAN_SCALE, help='the maximum scale used to cut bridges. A certain maximum scale cuts ' 'bridges up to a width (in voxels) of 2x the provided scale. Setting ' diff --git a/lib/mrtrix3/dwi2mask/mtnorm.py b/lib/mrtrix3/dwi2mask/mtnorm.py index 340e923723..03c34b67b4 100644 --- a/lib/mrtrix3/dwi2mask/mtnorm.py +++ b/lib/mrtrix3/dwi2mask/mtnorm.py @@ -45,19 +45,21 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn, help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut, help='The output mask image') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-init_mask', + type=app.Parser.ImageIn, metavar='image', help='Provide an initial brain mask, which will constrain the response function estimation ' '(if omitted, the default dwi2mask algorithm will be used)') options.add_argument('-lmax', + type=app.Parser.SequenceInt, metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') options.add_argument('-threshold', - type=float, + type=app.Parser.Float(0.0, 1.0), metavar='value', default=THRESHOLD_DEFAULT, help='the threshold on the total tissue density sum image used to derive the brain mask; default is ' + str(THRESHOLD_DEFAULT)) diff --git a/lib/mrtrix3/dwi2mask/synthstrip.py b/lib/mrtrix3/dwi2mask/synthstrip.py index d02070ee94..9608f6594c 100644 --- a/lib/mrtrix3/dwi2mask/synthstrip.py +++ b/lib/mrtrix3/dwi2mask/synthstrip.py @@ -35,9 +35,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options=parser.add_argument_group('Options specific to the \'Synthstrip\' algorithm') options.add_argument('-stripped', help='The output stripped image') options.add_argument('-gpu', action='store_true', default=False, help='Use the GPU') - options.add_argument('-model', metavar='file', help='Alternative model weights') + options.add_argument('-model', type=app.Parser.FileIn, metavar='file', help='Alternative model weights') options.add_argument('-nocsf', action='store_true', default=False, help='Compute the immediate boundary of brain matter excluding surrounding CSF') - options.add_argument('-border', type=int, help='Control the boundary distance from the brain') + options.add_argument('-border', type=app.Parser.Float(), help='Control the boundary distance from the brain') diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index d042ed921c..bd82610a6c 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -28,7 +28,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the \'trace\' algorithm') options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='Comma-separated list of shells used to generate trace-weighted images for masking') options.add_argument('-clean_scale', - type=int, + type=app.Parser.Int(0), default=DEFAULT_CLEAN_SCALE, help='the maximum scale used to cut bridges. A certain maximum scale cuts ' 'bridges up to a width (in voxels) of 2x the provided scale. Setting ' diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index fa71259bf5..135e4b6ef8 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -36,11 +36,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('out_gm', type=app.Parser.FileOut(), help='Output GM response function text file') parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response function text file') options = parser.add_argument_group('Options for the \'dhollander\' algorithm') - options.add_argument('-erode', type=int, metavar='passes', default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') - options.add_argument('-fa', type=float, metavar='threshold', default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') - options.add_argument('-sfwm', type=float, metavar='percentage', default=0.5, help='Final number of single-fibre WM voxels to select, as a percentage of refined WM. (default: 0.5 per cent)') - options.add_argument('-gm', type=float, metavar='percentage', default=2.0, help='Final number of GM voxels to select, as a percentage of refined GM. (default: 2 per cent)') - options.add_argument('-csf', type=float, metavar='percentage', default=10.0, help='Final number of CSF voxels to select, as a percentage of refined CSF. (default: 10 per cent)') + options.add_argument('-erode', type=app.Parser.Int(0), metavar='passes', default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') + options.add_argument('-fa', type=app.Parser.Float(0.0, 1.0), metavar='threshold', default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') + options.add_argument('-sfwm', type=app.Parser.Float(0.0, 100.0), metavar='percentage', default=0.5, help='Final number of single-fibre WM voxels to select, as a percentage of refined WM. (default: 0.5 per cent)') + options.add_argument('-gm', type=app.Parser.Float(0.0, 100.0), metavar='percentage', default=2.0, help='Final number of GM voxels to select, as a percentage of refined GM. (default: 2 per cent)') + options.add_argument('-csf', type=app.Parser.Float(0.0, 100.0), metavar='percentage', default=10.0, help='Final number of CSF voxels to select, as a percentage of refined CSF. (default: 10 per cent)') options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, help='Use external dwi2response algorithm for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + ') (default: built-in Dhollander 2019)') diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index d1ec976e1a..b0710bc0fd 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -27,9 +27,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'fa\' algorithm') - options.add_argument('-erode', type=int, metavar='passes', default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') - options.add_argument('-number', type=int, metavar='voxels', default=300, help='The number of highest-FA voxels to use') - options.add_argument('-threshold', type=float, metavar='value', help='Apply a hard FA threshold, rather than selecting the top voxels') + options.add_argument('-erode', type=app.Parser.Int(0), metavar='passes', default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') + options.add_argument('-number', type=app.Parser.Int(1), metavar='voxels', default=300, help='The number of highest-FA voxels to use') + options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), metavar='value', help='Apply a hard FA threshold, rather than selecting the top voxels') parser.flag_mutually_exclusive_options( [ 'number', 'threshold' ] ) diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index 895cf21ff5..50037f0622 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -35,10 +35,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response text file') options = parser.add_argument_group('Options specific to the \'msmt_5tt\' algorithm') options.add_argument('-dirs', type=app.Parser.ImageIn(), metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') - options.add_argument('-fa', type=float, metavar='value', default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') - options.add_argument('-pvf', type=float, metavar='fraction', default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') + options.add_argument('-fa', type=app.Parser.Float(0.0, 1.0), metavar='value', default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') + options.add_argument('-pvf', type=app.Parser.Float(0.0, 1.0), metavar='fraction', default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, default='tournier', help='dwi2response algorithm to use for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + '; default: tournier)') - options.add_argument('-sfwm_fa_threshold', type=float, metavar='value', help='Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, which is passed to the -threshold option of the fa algorithm (warning: overrides -wm_algo option)') + options.add_argument('-sfwm_fa_threshold', type=app.Parser.Float(0.0, 1.0), metavar='value', help='Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, which is passed to the -threshold option of the fa algorithm (warning: overrides -wm_algo option)') diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index 294555c4c5..dbab94380d 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -27,9 +27,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tax\' algorithm') - options.add_argument('-peak_ratio', type=float, metavar='value', default=0.1, help='Second-to-first-peak amplitude ratio threshold') - options.add_argument('-max_iters', type=int, metavar='iterations', default=20, help='Maximum number of iterations') - options.add_argument('-convergence', type=float, metavar='percentage', default=0.5, help='Percentile change in any RF coefficient required to continue iterating') + options.add_argument('-peak_ratio', type=app.Parser.Float(0.0, 1.0), metavar='value', default=0.1, help='Second-to-first-peak amplitude ratio threshold') + options.add_argument('-max_iters', type=app.Parser.Int(0), metavar='iterations', default=20, help='Maximum number of iterations (set to 0 to force convergence)') + options.add_argument('-convergence', type=app.Parser.Float(0.0), metavar='percentage', default=0.5, help='Percentile change in any RF coefficient required to continue iterating') @@ -63,7 +63,7 @@ def execute(): #pylint: disable=unused-variable progress = app.ProgressBar('Optimising') iteration = 0 - while iteration < app.ARGS.max_iters: + while iteration < app.ARGS.max_iters or not app.ARGS.max_iters: prefix = 'iter' + str(iteration) + '_' # How to initialise response function? diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index 6411bf073d..e6fb866c16 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -27,10 +27,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') options = parser.add_argument_group('Options specific to the \'tournier\' algorithm') - options.add_argument('-number', type=int, metavar='voxels', default=300, help='Number of single-fibre voxels to use when calculating response function') - options.add_argument('-iter_voxels', type=int, metavar='voxels', default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') - options.add_argument('-dilate', type=int, metavar='passes', default=1, help='Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration') - options.add_argument('-max_iters', type=int, metavar='iterations', default=10, help='Maximum number of iterations') + options.add_argument('-number', type=app.Parser.Int(1), metavar='voxels', default=300, help='Number of single-fibre voxels to use when calculating response function') + options.add_argument('-iter_voxels', type=app.Parser.Int(0), metavar='voxels', default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') + options.add_argument('-dilate', type=app.Parser.Int(1), metavar='passes', default=1, help='Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration') + options.add_argument('-max_iters', type=app.Parser.Int(0), metavar='iterations', default=10, help='Maximum number of iterations (set to 0 to force convergence)') @@ -59,9 +59,6 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.lmax: lmax_option = ' -lmax ' + ','.join(str(item) for item in app.ARGS.lmax) - if app.ARGS.max_iters < 2: - raise MRtrixError('Number of iterations must be at least 2') - progress = app.ProgressBar('Optimising') iter_voxels = app.ARGS.iter_voxels @@ -71,7 +68,7 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError ('Number of selected voxels (-iter_voxels) must be greater than number of voxels desired (-number)') iteration = 0 - while iteration < app.ARGS.max_iters: + while iteration < app.ARGS.max_iters or not app.ARGS.max_iters: prefix = 'iter' + str(iteration) + '_' if iteration == 0: diff --git a/lib/mrtrix3/dwibiascorrect/mtnorm.py b/lib/mrtrix3/dwibiascorrect/mtnorm.py index dbbcd5350c..08fa2dca0b 100644 --- a/lib/mrtrix3/dwibiascorrect/mtnorm.py +++ b/lib/mrtrix3/dwibiascorrect/mtnorm.py @@ -44,10 +44,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Tabbara, R.; Rosnarho-Tornstrand, J.; Tournier, J.-D.; Raffelt, D. & Connelly, A. ' 'Multi-tissue log-domain intensity and inhomogeneity normalisation for quantitative apparent fibre density. ' 'In Proc. ISMRM, 2021, 29, 2472') - parser.add_argument('input', help='The input image series to be corrected') - parser.add_argument('output', help='The output corrected image series') + parser.add_argument('input', type=app.Parser.ImageIn, help='The input image series to be corrected') + parser.add_argument('output', type=app.Parser.ImageOut, help='The output corrected image series') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', + type=app.Parser.SequenceInt, metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') diff --git a/lib/mrtrix3/dwinormalise/group.py b/lib/mrtrix3/dwinormalise/group.py index 3a99805b61..7be4759902 100644 --- a/lib/mrtrix3/dwinormalise/group.py +++ b/lib/mrtrix3/dwinormalise/group.py @@ -18,6 +18,8 @@ from mrtrix3 import app, image, path, run, utils +FA_THRESHOLD_DEFAULT = 0.4 + def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('group', parents=[base_parser]) @@ -30,7 +32,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('output_dir', type=app.Parser.DirectoryOut(), help='The output directory containing all of the intensity normalised DWI images') parser.add_argument('fa_template', type=app.Parser.ImageOut(), help='The output population-specific FA template, which is thresholded to estimate a white matter mask') parser.add_argument('wm_mask', type=app.Parser.ImageOut(), help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') - parser.add_argument('-fa_threshold', default='0.4', metavar='value', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: 0.4)') + parser.add_argument('-fa_threshold', type=app.Parser.Float(0.0, 1.0), default=FA_THRESHOLD_DEFAULT, metavar='value', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: ' + str(FA_THRESHOLD_DEFAULT) + ')') @@ -104,7 +106,7 @@ def __init__(self, filename, prefix, mask_filename = ''): + ('' if app.DO_CLEANUP else ' -nocleanup')) app.console('Generating WM mask in template space') - run.command('mrthreshold fa_template.mif -abs ' + app.ARGS.fa_threshold + ' template_wm_mask.mif') + run.command('mrthreshold fa_template.mif -abs ' + str(app.ARGS.fa_threshold) + ' template_wm_mask.mif') progress = app.ProgressBar('Intensity normalising subject images', len(input_list)) utils.make_dir(path.from_user(app.ARGS.output_dir, False)) diff --git a/lib/mrtrix3/dwinormalise/manual.py b/lib/mrtrix3/dwinormalise/manual.py index 7f339e3dba..c2b311fa5a 100644 --- a/lib/mrtrix3/dwinormalise/manual.py +++ b/lib/mrtrix3/dwinormalise/manual.py @@ -29,8 +29,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('input_dwi', type=app.Parser.ImageIn(), help='The input DWI series') parser.add_argument('input_mask', type=app.Parser.ImageIn(), help='The mask within which a reference b=0 intensity will be sampled') parser.add_argument('output_dwi', type=app.Parser.ImageOut(), help='The output intensity-normalised DWI series') - parser.add_argument('-intensity', type=float, metavar='value', default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value (Default: ' + str(DEFAULT_TARGET_INTENSITY) + ')') - parser.add_argument('-percentile', type=int, metavar='value', help='Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value') + parser.add_argument('-intensity', type=app.Parser.Float(0.0), metavar='value', default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value (Default: ' + str(DEFAULT_TARGET_INTENSITY) + ')') + parser.add_argument('-percentile', type=app.Parser.Float(0.0, 100.0), metavar='value', help='Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value') app.add_dwgrad_import_options(parser) @@ -49,8 +49,6 @@ def execute(): #pylint: disable=unused-variable grad_option = ' -fslgrad ' + path.from_user(app.ARGS.fslgrad[0]) + ' ' + path.from_user(app.ARGS.fslgrad[1]) if app.ARGS.percentile: - if app.ARGS.percentile < 0.0 or app.ARGS.percentile > 100.0: - raise MRtrixError('-percentile value must be between 0 and 100') intensities = [float(value) for value in run.command('dwiextract ' + path.from_user(app.ARGS.input_dwi) + grad_option + ' -bzero - | ' + \ 'mrmath - mean - -axis 3 | ' + \ 'mrdump - -mask ' + path.from_user(app.ARGS.input_mask)).stdout.splitlines()] diff --git a/lib/mrtrix3/dwinormalise/mtnorm.py b/lib/mrtrix3/dwinormalise/mtnorm.py index 7f9c5a7589..d8a49a96b6 100644 --- a/lib/mrtrix3/dwinormalise/mtnorm.py +++ b/lib/mrtrix3/dwinormalise/mtnorm.py @@ -49,24 +49,27 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The normalised DWI series') + parser.add_argument('input', type=app.Parser.ImageIn, help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut, help='The normalised DWI series') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', + type=app.Parser.SequenceInt, metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') options.add_argument('-mask', + type=app.Parser.ImageIn, metavar='image', help='Provide a mask image for relevant calculations ' '(if not provided, the default dwi2mask algorithm will be used)') options.add_argument('-reference', - type=float, + type=app.Parser.Float(0.0), metavar='value', default=REFERENCE_INTENSITY, help='Set the target CSF b=0 intensity in the output DWI series ' '(default: ' + str(REFERENCE_INTENSITY) + ')') options.add_argument('-scale', + app.Parser.FileOut, metavar='file', help='Write the scaling factor applied to the DWI series to a text file') app.add_dwgrad_import_options(parser) From 18e685014256d2f65d73ae3ed268e6fc90a40090 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 5 Feb 2024 10:33:03 +1100 Subject: [PATCH 30/75] Python: Broad fixing of typed command-line argument usage --- bin/dwibiasnormmask | 2 +- bin/population_template | 40 +++++++++---------- docs/reference/commands/dwi2mask.rst | 8 ++-- docs/reference/commands/dwi2response.rst | 4 +- docs/reference/commands/dwibiascorrect.rst | 8 ++-- docs/reference/commands/dwibiasnormmask.rst | 4 +- docs/reference/commands/dwinormalise.rst | 6 +-- .../commands/population_template.rst | 2 +- lib/mrtrix3/app.py | 36 ++++++++++------- lib/mrtrix3/dwi2mask/mtnorm.py | 8 ++-- lib/mrtrix3/dwi2mask/synthstrip.py | 10 ++--- lib/mrtrix3/dwi2mask/trace.py | 2 +- lib/mrtrix3/dwibiascorrect/mtnorm.py | 6 +-- lib/mrtrix3/dwinormalise/manual.py | 1 - lib/mrtrix3/dwinormalise/mtnorm.py | 10 ++--- 15 files changed, 77 insertions(+), 70 deletions(-) diff --git a/bin/dwibiasnormmask b/bin/dwibiasnormmask index e71c42144e..2d62b3b65e 100755 --- a/bin/dwibiasnormmask +++ b/bin/dwibiasnormmask @@ -108,7 +108,7 @@ def usage(cmdline): #pylint: disable=unused-variable 'set to 0 to proceed until convergence') internal_options.add_argument('-mask_algo', choices=MASK_ALGOS, metavar='algorithm', help='The algorithm to use for mask estimation, potentially based on the ODF sum image (see Description); default: ' + MASK_ALGO_DEFAULT) - internal_options.add_argument('-lmax', metavar='values', type=app.Parser.SequenceInt, + internal_options.add_argument('-lmax', metavar='values', type=app.Parser.SequenceInt(), help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') app.add_dwgrad_import_options(cmdline) diff --git a/bin/population_template b/bin/population_template index 8775ef5f67..c6d91edc65 100755 --- a/bin/population_template +++ b/bin/population_template @@ -59,43 +59,43 @@ def usage(cmdline): #pylint: disable=unused-variable 'with the pairs corresponding to different contrasts provided sequentially.') options = cmdline.add_argument_group('Multi-contrast options') - options.add_argument('-mc_weight_initial_alignment', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') - options.add_argument('-mc_weight_rigid', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_affine', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_nl', type=app.Parser().SequenceFloat(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_initial_alignment', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') + options.add_argument('-mc_weight_rigid', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_affine', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_nl', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') linoptions = cmdline.add_argument_group('Options for the linear registration') linoptions.add_argument('-linear_no_pause', action='store_true', help='Do not pause the script if a linear registration seems implausible') linoptions.add_argument('-linear_no_drift_correction', action='store_true', help='Deactivate correction of template appearance (scale and shear) over iterations') linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), none (no robust estimator). Default: none.') - linoptions.add_argument('-rigid_scale', type=app.Parser().SequenceFloat(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) - linoptions.add_argument('-rigid_lmax', type=app.Parser().SequenceInt(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) - linoptions.add_argument('-rigid_niter', type=app.Parser().SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') - linoptions.add_argument('-affine_scale', type=app.Parser().SequenceFloat(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) - linoptions.add_argument('-affine_lmax', type=app.Parser().SequenceInt(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) - linoptions.add_argument('-affine_niter', type=app.Parser().SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-rigid_scale', type=app.Parser.SequenceFloat(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) + linoptions.add_argument('-rigid_lmax', type=app.Parser.SequenceInt(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) + linoptions.add_argument('-rigid_niter', type=app.Parser.SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-affine_scale', type=app.Parser.SequenceFloat(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) + linoptions.add_argument('-affine_lmax', type=app.Parser.SequenceInt(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) + linoptions.add_argument('-affine_niter', type=app.Parser.SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') nloptions = cmdline.add_argument_group('Options for the non-linear registration') - nloptions.add_argument('-nl_scale', type=app.Parser().SequenceFloat(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) - nloptions.add_argument('-nl_lmax', type=app.Parser().SequenceInt(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) - nloptions.add_argument('-nl_niter', type=app.Parser().SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) + nloptions.add_argument('-nl_scale', type=app.Parser.SequenceFloat(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) + nloptions.add_argument('-nl_lmax', type=app.Parser.SequenceInt(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) + nloptions.add_argument('-nl_niter', type=app.Parser.SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) nloptions.add_argument('-nl_update_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_UPDATE_SMOOTH, help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_UPDATE_SMOOTH) + ' x voxel_size)') nloptions.add_argument('-nl_disp_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_DISP_SMOOTH, help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_DISP_SMOOTH) + ' x voxel_size)') nloptions.add_argument('-nl_grad_step', type=app.Parser.Float(0.0), default=DEFAULT_NL_GRAD_STEP, help='The gradient step size for non-linear registration (Default: ' + str(DEFAULT_NL_GRAD_STEP) + ')') options = cmdline.add_argument_group('Input, output and general options') options.add_argument('-type', choices=REGISTRATION_MODES, help='Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"' + x + '"' for x in REGISTRATION_MODES if "_" in x), default='rigid_affine_nonlinear') - options.add_argument('-voxel_size', type=app.Parser().SequenceFloat(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') + options.add_argument('-voxel_size', type=app.Parser.SequenceFloat(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') options.add_argument('-initial_alignment', choices=INITIAL_ALIGNMENT, default='mass', help='Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none".') - options.add_argument('-mask_dir', type=app.Parser().DirectoryIn(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') - options.add_argument('-warp_dir', type=app.Parser().DirectoryOut(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') - options.add_argument('-transformed_dir', type=app.Parser().DirectoryOut(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') - options.add_argument('-linear_transformations_dir', type=app.Parser().DirectoryOut(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') - options.add_argument('-template_mask', type=app.Parser().ImageOut(), help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') + options.add_argument('-mask_dir', type=app.Parser.DirectoryIn(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') + options.add_argument('-warp_dir', type=app.Parser.DirectoryOut(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') + options.add_argument('-transformed_dir', type=app.Parser.DirectoryOut(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') + options.add_argument('-linear_transformations_dir', type=app.Parser.DirectoryOut(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') + options.add_argument('-template_mask', type=app.Parser.ImageOut(), help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') options.add_argument('-noreorientation', action='store_true', help='Turn off FOD reorientation in mrregister. Reorientation is on by default if the number of volumes in the 4th dimension corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series (i.e. 6, 15, 28, 45, 66 etc)') options.add_argument('-leave_one_out', choices=LEAVE_ONE_OUT, default='auto', help='Register each input image to a template that does not contain that image. Valid choices: ' + ', '.join(LEAVE_ONE_OUT) + '. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) ') options.add_argument('-aggregate', choices=AGGREGATION_MODES, help='Measure used to aggregate information from transformed images to the template image. Valid choices: %s. Default: mean' % ', '.join(AGGREGATION_MODES)) - options.add_argument('-aggregation_weights', type=app.Parser().FileIn(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') + options.add_argument('-aggregation_weights', type=app.Parser.FileIn(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') options.add_argument('-nanmask', action='store_true', help='Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. Only works if -mask_dir has been input.') options.add_argument('-copy_input', action='store_true', help='Copy input images and masks into local scratch directory.') options.add_argument('-delete_temporary_files', action='store_true', help='Delete temporary files from scratch directory during template creation.') diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index 5600dc884a..9cd7420212 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -913,7 +913,7 @@ Options specific to the "mtnorm" algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -924,7 +924,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ @@ -1019,7 +1019,7 @@ Options specific to the 'Synthstrip' algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -1030,7 +1030,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index db2486f16d..c352fdfd46 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -573,7 +573,7 @@ Options specific to the 'tax' algorithm - **-peak_ratio value** Second-to-first-peak amplitude ratio threshold -- **-max_iters iterations** Maximum number of iterations +- **-max_iters iterations** Maximum number of iterations (set to 0 to force convergence) - **-convergence percentage** Percentile change in any RF coefficient required to continue iterating @@ -683,7 +683,7 @@ Options specific to the 'tournier' algorithm - **-dilate passes** Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration -- **-max_iters iterations** Maximum number of iterations +- **-max_iters iterations** Maximum number of iterations (set to 0 to force convergence) Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwibiascorrect.rst b/docs/reference/commands/dwibiascorrect.rst index bf41865725..19f9d2716d 100644 --- a/docs/reference/commands/dwibiascorrect.rst +++ b/docs/reference/commands/dwibiascorrect.rst @@ -336,16 +336,16 @@ Options specific to the "mtnorm" algorithm Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format Options common to all dwibiascorrect algorithms ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-mask image** Manually provide a mask image for bias field estimation +- **-mask image** Manually provide an input mask image for bias field estimation -- **-bias image** Output the estimated bias field +- **-bias image** Output an image containing the estimated bias field Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -354,7 +354,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwibiasnormmask.rst b/docs/reference/commands/dwibiasnormmask.rst index 0df9e89030..73875ec8d4 100644 --- a/docs/reference/commands/dwibiasnormmask.rst +++ b/docs/reference/commands/dwibiasnormmask.rst @@ -40,7 +40,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -75,7 +75,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwinormalise.rst b/docs/reference/commands/dwinormalise.rst index 130cb050b3..8c179645b3 100644 --- a/docs/reference/commands/dwinormalise.rst +++ b/docs/reference/commands/dwinormalise.rst @@ -176,7 +176,7 @@ dwinormalise manual Synopsis -------- -Intensity normalise a DWI series based on the b=0 signal within a manually-supplied supplied mask +Intensity normalise a DWI series based on the b=0 signal within a supplied mask Usage ----- @@ -292,7 +292,7 @@ Options Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-grad** Provide the diffusion gradient table in MRtrix format +- **-grad file** Provide the diffusion gradient table in MRtrix format - **-fslgrad bvecs bvals** Provide the diffusion gradient table in FSL bvecs/bvals format @@ -314,7 +314,7 @@ Additional standard options for Python scripts - **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. -- **-continue ** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. Standard options ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/population_template.rst b/docs/reference/commands/population_template.rst index 4889845c8e..dd96d9ebfa 100644 --- a/docs/reference/commands/population_template.rst +++ b/docs/reference/commands/population_template.rst @@ -15,7 +15,7 @@ Usage population_template input_dir template [ options ] -- *input_dir*: Directory containing all input images of a given contrast +- *input_dir*: Input directory containing all images of a given contrast - *template*: Output template image Description diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index b3746b54c5..7d2360d71f 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -585,7 +585,7 @@ def _get_message(self): # The Parser class is responsible for setting up command-line parsing for the script. -# This includes proper CONFIGuration of the argparse functionality, adding standard options +# This includes proper configuration of the argparse functionality, adding standard options # that are common for all scripts, providing a custom help page that is consistent with the # MRtrix3 binaries, and defining functions for exporting the help page for the purpose of # automated self-documentation. @@ -616,11 +616,11 @@ def __call__(self, input_value): def _typestring(): return 'BOOL' - def Int(min_value=None, max_value=None): + def Int(min_value=None, max_value=None): # pylint: disable=invalid-name assert min_value is None or isinstance(min_value, int) assert max_value is None or isinstance(max_value, int) assert min_value is None or max_value is None or max_value >= min_value - class Checker(Parser.CustomTypeBase): + class IntChecker(Parser.CustomTypeBase): def __call__(self, input_value): try: value = int(input_value) @@ -633,14 +633,14 @@ def __call__(self, input_value): return value @staticmethod def _typestring(): - return 'INT ' + ('-9223372036854775808' if min_value is None else str(min_value)) + ' ' + ('9223372036854775807' if max_value is None else str(max_value)) - return Checker + return 'INT ' + (str(-sys.maxsize - 1) if min_value is None else str(min_value)) + ' ' + (str(sys.maxsize) if max_value is None else str(max_value)) + return IntChecker() - def Float(min_value=None, max_value=None): + def Float(min_value=None, max_value=None): # pylint: disable=invalid-name assert min_value is None or isinstance(min_value, float) assert max_value is None or isinstance(max_value, float) assert min_value is None or max_value is None or max_value >= min_value - class Checker(Parser.CustomTypeBase): + class FloatChecker(Parser.CustomTypeBase): def __call__(self, input_value): try: value = float(input_value) @@ -654,7 +654,7 @@ def __call__(self, input_value): @staticmethod def _typestring(): return 'FLOAT ' + ('-inf' if min_value is None else str(min_value)) + ' ' + ('inf' if max_value is None else str(max_value)) - return Checker + return FloatChecker() class SequenceInt(CustomTypeBase): def __call__(self, input_value): @@ -701,6 +701,7 @@ def __call__(self, input_value): if not os.path.isfile(input_value): raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a file') return input_value + @staticmethod def _typestring(): return 'FILEIN' @@ -743,9 +744,11 @@ def _typestring(): class TracksOut(FileOut): def __call__(self, input_value): + super().__call__(input_value) if not input_value.endswith('.tck'): raise argparse.ArgumentTypeError('Output tractogram path "' + input_value + '" does not use the requisite ".tck" suffix') return input_value + @staticmethod def _typestring(): return 'TRACKSOUT' @@ -789,8 +792,8 @@ def __init__(self, *args_in, **kwargs_in): standard_options.add_argument('-version', action='store_true', help='display version information and exit.') script_options = self.add_argument_group('Additional standard options for Python scripts') script_options.add_argument('-nocleanup', action='store_true', help='do not delete intermediate files during script execution, and do not delete scratch directory at script completion.') - script_options.add_argument('-scratch', type=Parser.DirectoryOut, metavar='/path/to/scratch/', help='manually specify the path in which to generate the scratch directory.') - script_options.add_argument('-continue', nargs=2, dest='cont', metavar=('ScratchDir', 'LastFile'), help='continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file.') + script_options.add_argument('-scratch', type=Parser.DirectoryOut(), metavar='/path/to/scratch/', help='manually specify the path in which to generate the scratch directory.') + script_options.add_argument('-continue', type=Parser.Various(), nargs=2, dest='cont', metavar=('ScratchDir', 'LastFile'), help='continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file.') module_file = os.path.realpath (inspect.getsourcefile(inspect.stack()[-1][0])) self._is_project = os.path.abspath(os.path.join(os.path.dirname(module_file), os.pardir, 'lib', 'mrtrix3', 'app.py')) != os.path.abspath(__file__) try: @@ -1099,6 +1102,10 @@ def print_full_usage(self): def arg2str(arg): if arg.choices: return 'CHOICE ' + ' '.join(arg.choices) + if isinstance(arg.type, int) or arg.type is int: + return 'INT ' + str(-sys.maxsize - 1) + ' ' + str(sys.maxsize) + if isinstance(arg.type, float) or arg.type is float: + return 'FLOAT -inf inf' if isinstance(arg.type, str) or arg.type is str or arg.type is None: return 'TEXT' if isinstance(arg.type, Parser.CustomTypeBase): @@ -1335,9 +1342,10 @@ def _is_option_group(self, group): # Define functions for incorporating commonly-used command-line options / option groups def add_dwgrad_import_options(cmdline): #pylint: disable=unused-variable options = cmdline.add_argument_group('Options for importing the diffusion gradient table') - options.add_argument('-grad', type=Parser.FileIn, metavar='file', help='Provide the diffusion gradient table in MRtrix format') - options.add_argument('-fslgrad', type=Parser.FileIn, nargs=2, metavar=('bvecs', 'bvals'), help='Provide the diffusion gradient table in FSL bvecs/bvals format') + options.add_argument('-grad', type=Parser.FileIn(), metavar='file', help='Provide the diffusion gradient table in MRtrix format') + options.add_argument('-fslgrad', type=Parser.FileIn(), nargs=2, metavar=('bvecs', 'bvals'), help='Provide the diffusion gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'grad', 'fslgrad' ] ) + def read_dwgrad_import_options(): #pylint: disable=unused-variable from mrtrix3 import path #pylint: disable=import-outside-toplevel assert ARGS @@ -1352,8 +1360,8 @@ def read_dwgrad_import_options(): #pylint: disable=unused-variable def add_dwgrad_export_options(cmdline): #pylint: disable=unused-variable options = cmdline.add_argument_group('Options for exporting the diffusion gradient table') - options.add_argument('-export_grad_mrtrix', type=Parser.FileOut, metavar='grad', help='Export the final gradient table in MRtrix format') - options.add_argument('-export_grad_fsl', type=Parser.FileOut, nargs=2, metavar=('bvecs', 'bvals'), help='Export the final gradient table in FSL bvecs/bvals format') + options.add_argument('-export_grad_mrtrix', type=Parser.FileOut(), metavar='grad', help='Export the final gradient table in MRtrix format') + options.add_argument('-export_grad_fsl', type=Parser.FileOut(), nargs=2, metavar=('bvecs', 'bvals'), help='Export the final gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'export_grad_mrtrix', 'export_grad_fsl' ] ) diff --git a/lib/mrtrix3/dwi2mask/mtnorm.py b/lib/mrtrix3/dwi2mask/mtnorm.py index 03c34b67b4..a1f4c88301 100644 --- a/lib/mrtrix3/dwi2mask/mtnorm.py +++ b/lib/mrtrix3/dwi2mask/mtnorm.py @@ -45,16 +45,16 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') - parser.add_argument('input', type=app.Parser.ImageIn, help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut, help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-init_mask', - type=app.Parser.ImageIn, + type=app.Parser.ImageIn(), metavar='image', help='Provide an initial brain mask, which will constrain the response function estimation ' '(if omitted, the default dwi2mask algorithm will be used)') options.add_argument('-lmax', - type=app.Parser.SequenceInt, + type=app.Parser.SequenceInt(), metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') diff --git a/lib/mrtrix3/dwi2mask/synthstrip.py b/lib/mrtrix3/dwi2mask/synthstrip.py index 9608f6594c..f8f2296587 100644 --- a/lib/mrtrix3/dwi2mask/synthstrip.py +++ b/lib/mrtrix3/dwi2mask/synthstrip.py @@ -30,14 +30,14 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_description('This algorithm requires that the SynthStrip method be installed and available via PATH; ' 'this could be via Freesufer 7.3.0 or later, or the dedicated Singularity container.') parser.add_citation('A. Hoopes, J. S. Mora, A. V. Dalca, B. Fischl, M. Hoffmann. SynthStrip: Skull-Stripping for Any Brain Image. NeuroImage, 2022, 260, 119474', is_external=True) - parser.add_argument('input', help='The input DWI series') - parser.add_argument('output', help='The output mask image') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') options=parser.add_argument_group('Options specific to the \'Synthstrip\' algorithm') - options.add_argument('-stripped', help='The output stripped image') + options.add_argument('-stripped', type=app.Parser.ImageOut(), help='The output stripped image') options.add_argument('-gpu', action='store_true', default=False, help='Use the GPU') - options.add_argument('-model', type=app.Parser.FileIn, metavar='file', help='Alternative model weights') + options.add_argument('-model', type=app.Parser.FileIn(), metavar='file', help='Alternative model weights') options.add_argument('-nocsf', action='store_true', default=False, help='Compute the immediate boundary of brain matter excluding surrounding CSF') - options.add_argument('-border', type=app.Parser.Float(), help='Control the boundary distance from the brain') + options.add_argument('-border', type=float, help='Control the boundary distance from the brain') diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index bd82610a6c..99b38a050a 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -37,7 +37,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable iter_options.add_argument('-iterative', action='store_true', help='(EXPERIMENTAL) Iteratively refine the weights for combination of per-shell trace-weighted images prior to thresholding') - iter_options.add_argument('-max_iters', type=int, default=DEFAULT_MAX_ITERS, help='Set the maximum number of iterations for the algorithm (default: ' + str(DEFAULT_MAX_ITERS) + ')') + iter_options.add_argument('-max_iters', type=app.Parser.Int(1), default=DEFAULT_MAX_ITERS, help='Set the maximum number of iterations for the algorithm (default: ' + str(DEFAULT_MAX_ITERS) + ')') diff --git a/lib/mrtrix3/dwibiascorrect/mtnorm.py b/lib/mrtrix3/dwibiascorrect/mtnorm.py index 08fa2dca0b..5a7276a068 100644 --- a/lib/mrtrix3/dwibiascorrect/mtnorm.py +++ b/lib/mrtrix3/dwibiascorrect/mtnorm.py @@ -44,11 +44,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Tabbara, R.; Rosnarho-Tornstrand, J.; Tournier, J.-D.; Raffelt, D. & Connelly, A. ' 'Multi-tissue log-domain intensity and inhomogeneity normalisation for quantitative apparent fibre density. ' 'In Proc. ISMRM, 2021, 29, 2472') - parser.add_argument('input', type=app.Parser.ImageIn, help='The input image series to be corrected') - parser.add_argument('output', type=app.Parser.ImageOut, help='The output corrected image series') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input image series to be corrected') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', - type=app.Parser.SequenceInt, + type=app.Parser.SequenceInt(), metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') diff --git a/lib/mrtrix3/dwinormalise/manual.py b/lib/mrtrix3/dwinormalise/manual.py index c2b311fa5a..61dffd009c 100644 --- a/lib/mrtrix3/dwinormalise/manual.py +++ b/lib/mrtrix3/dwinormalise/manual.py @@ -14,7 +14,6 @@ # For more details, see http://www.mrtrix.org/. import math -from mrtrix3 import MRtrixError from mrtrix3 import app, path, run diff --git a/lib/mrtrix3/dwinormalise/mtnorm.py b/lib/mrtrix3/dwinormalise/mtnorm.py index d8a49a96b6..38f1a1f091 100644 --- a/lib/mrtrix3/dwinormalise/mtnorm.py +++ b/lib/mrtrix3/dwinormalise/mtnorm.py @@ -49,16 +49,16 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') - parser.add_argument('input', type=app.Parser.ImageIn, help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut, help='The normalised DWI series') + parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') + parser.add_argument('output', type=app.Parser.ImageOut(), help='The normalised DWI series') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', - type=app.Parser.SequenceInt, + type=app.Parser.SequenceInt(), metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') options.add_argument('-mask', - type=app.Parser.ImageIn, + type=app.Parser.ImageIn(), metavar='image', help='Provide a mask image for relevant calculations ' '(if not provided, the default dwi2mask algorithm will be used)') @@ -69,7 +69,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable help='Set the target CSF b=0 intensity in the output DWI series ' '(default: ' + str(REFERENCE_INTENSITY) + ')') options.add_argument('-scale', - app.Parser.FileOut, + type=app.Parser.FileOut(), metavar='file', help='Write the scaling factor applied to the DWI series to a text file') app.add_dwgrad_import_options(parser) From d511e3d0765c380986b1fbfd2d2c2671a7034612 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 13 Feb 2024 18:29:15 +1100 Subject: [PATCH 31/75] Python API: Multiple related changes - For command-line arguments that involve filesystem paths, perform immediate translation to absolute filesystem path for the contents of app.ARGS. Further, store such information using a custom type that will add shell escape double-quotes if contributing to an F-string. - Utilise Python3 F-strings throughout code base, both for readability and to mitigate chances of developers using string addition operation on filesystem paths (whether user-specified or absolute paths to the scratch directory or its contents) that may contain whitespace. - Remove some code that provided functionality for versions of Python that do not support F-strings, since those Python versions will no longer be supported. - Remove functions app.make_scratch_dir() and app.goto_scratch_dir(); instead, new function app.activate_scratch_dir() will both create that directory and change the current working directory to that location. This change coincides with the immediate translation of user-specified filesystem paths to absolute paths, such that a change in working directory should not be consequential for executed commands. - Perform some line wrapping of Python code to improve readability. - Remove many checks of user inputs that are made redundant by the enforcement of values at the time of command-line parsing. - Remove functions path.from_user() and path.to_scratch(). The former is obviated by the immediate translation of user-specified filesystem paths to absolute paths; the second is obviated by changing the current working directory to the scratch directory upon creation, such that simple string file names can be specified. Class app.ScratchPath can nevertheless be instantiated if one wants an absolute path to a location within the scratch directory. - For some flags provided by algorithms to the corresponding interface commands, change those flags from functions to const variables. - Improve conformation of use of single-quotes for Python strings (particularly in population_template). --- bin/5ttgen | 30 +- bin/blend | 16 +- bin/dwi2mask | 35 +- bin/dwi2response | 84 ++- bin/dwibiascorrect | 40 +- bin/dwibiasnormmask | 259 ++++--- bin/dwicat | 70 +- bin/dwifslpreproc | 762 +++++++++++-------- bin/dwigradcheck | 77 +- bin/dwinormalise | 7 +- bin/dwishellmath | 50 +- bin/for_each | 191 +++-- bin/labelsgmfix | 98 ++- bin/mask2glass | 64 +- bin/mrtrix_cleanup | 55 +- bin/population_template | 972 +++++++++++++++---------- bin/responsemean | 60 +- lib/mrtrix3/_5ttgen/freesurfer.py | 69 +- lib/mrtrix3/_5ttgen/fsl.py | 185 +++-- lib/mrtrix3/_5ttgen/gif.py | 46 +- lib/mrtrix3/_5ttgen/hsvs.py | 412 ++++++----- lib/mrtrix3/app.py | 619 +++++++++------- lib/mrtrix3/dwi2mask/3dautomask.py | 113 +-- lib/mrtrix3/dwi2mask/ants.py | 64 +- lib/mrtrix3/dwi2mask/b02template.py | 348 +++++---- lib/mrtrix3/dwi2mask/consensus.py | 108 +-- lib/mrtrix3/dwi2mask/fslbet.py | 69 +- lib/mrtrix3/dwi2mask/hdbet.py | 36 +- lib/mrtrix3/dwi2mask/legacy.py | 29 +- lib/mrtrix3/dwi2mask/mean.py | 47 +- lib/mrtrix3/dwi2mask/mtnorm.py | 115 ++- lib/mrtrix3/dwi2mask/synthstrip.py | 74 +- lib/mrtrix3/dwi2mask/trace.py | 83 ++- lib/mrtrix3/dwi2response/dhollander.py | 242 +++--- lib/mrtrix3/dwi2response/fa.py | 79 +- lib/mrtrix3/dwi2response/manual.py | 74 +- lib/mrtrix3/dwi2response/msmt_5tt.py | 154 ++-- lib/mrtrix3/dwi2response/tax.py | 124 ++-- lib/mrtrix3/dwi2response/tournier.py | 149 ++-- lib/mrtrix3/dwibiascorrect/ants.py | 67 +- lib/mrtrix3/dwibiascorrect/fsl.py | 60 +- lib/mrtrix3/dwibiascorrect/mtnorm.py | 93 ++- lib/mrtrix3/dwinormalise/group.py | 129 ++-- lib/mrtrix3/dwinormalise/manual.py | 59 +- lib/mrtrix3/dwinormalise/mtnorm.py | 97 ++- lib/mrtrix3/fsl.py | 48 +- lib/mrtrix3/image.py | 71 +- lib/mrtrix3/matrix.py | 43 +- lib/mrtrix3/path.py | 53 +- lib/mrtrix3/phaseencoding.py | 20 +- lib/mrtrix3/run.py | 104 +-- lib/mrtrix3/utils.py | 15 +- testing/pylint.rc | 2 +- testing/scripts/tests/dwi2mask | 2 +- 54 files changed, 3969 insertions(+), 3003 deletions(-) diff --git a/bin/5ttgen b/bin/5ttgen index 8fc341f1f3..ccb5522d56 100755 --- a/bin/5ttgen +++ b/bin/5ttgen @@ -22,13 +22,26 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Generate a 5TT image suitable for ACT') - cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. Anatomically-constrained tractography: Improved diffusion MRI streamlines tractography through effective use of anatomical information. NeuroImage, 2012, 62, 1924-1938') - cmdline.add_description('5ttgen acts as a \'master\' script for generating a five-tissue-type (5TT) segmented tissue image suitable for use in Anatomically-Constrained Tractography (ACT). A range of different algorithms are available for completing this task. When using this script, the name of the algorithm to be used must appear as the first argument on the command-line after \'5ttgen\'. The subsequent compulsory arguments and options available depend on the particular algorithm being invoked.') - cmdline.add_description('Each algorithm available also has its own help page, including necessary references; e.g. to see the help page of the \'fsl\' algorithm, type \'5ttgen fsl\'.') + cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. ' + 'Anatomically-constrained tractography: Improved diffusion MRI streamlines tractography through effective use of anatomical information. ' + 'NeuroImage, 2012, 62, 1924-1938') + cmdline.add_description('5ttgen acts as a "master" script for generating a five-tissue-type (5TT) segmented tissue image suitable for use in Anatomically-Constrained Tractography (ACT). ' + 'A range of different algorithms are available for completing this task. ' + 'When using this script, the name of the algorithm to be used must appear as the first argument on the command-line after "5ttgen". ' + 'The subsequent compulsory arguments and options available depend on the particular algorithm being invoked.') + cmdline.add_description('Each algorithm available also has its own help page, including necessary references; ' + 'e.g. to see the help page of the "fsl" algorithm, type "5ttgen fsl".') common_options = cmdline.add_argument_group('Options common to all 5ttgen algorithms') - common_options.add_argument('-nocrop', action='store_true', default=False, help='Do NOT crop the resulting 5TT image to reduce its size (keep the same dimensions as the input image)') - common_options.add_argument('-sgm_amyg_hipp', action='store_true', default=False, help='Represent the amygdalae and hippocampi as sub-cortical grey matter in the 5TT image') + common_options.add_argument('-nocrop', + action='store_true', + default=False, + help='Do NOT crop the resulting 5TT image to reduce its size ' + '(keep the same dimensions as the input image)') + common_options.add_argument('-sgm_amyg_hipp', + action='store_true', + default=False, + help='Represent the amygdalae and hippocampi as sub-cortical grey matter in the 5TT image') # Import the command-line settings for all algorithms found in the relevant directory algorithm.usage(cmdline) @@ -41,12 +54,9 @@ def execute(): #pylint: disable=unused-variable # Find out which algorithm the user has requested alg = algorithm.get_module(app.ARGS.algorithm) - alg.check_output_paths() - - app.make_scratch_dir() - alg.get_inputs() - app.goto_scratch_dir() + app.activate_scratch_dir() + # TODO Have algorithm return path to image to check alg.execute() stderr = run.command('5ttcheck result.mif').stderr diff --git a/bin/blend b/bin/blend index ebe03f07cd..fe0d86332f 100755 --- a/bin/blend +++ b/bin/blend @@ -21,8 +21,8 @@ import sys if len(sys.argv) <= 1: sys.stderr.write('A script to blend two sets of movie frames together with a desired overlap.\n') sys.stderr.write('The input arguments are two folders containing the movie frames ' - '(eg. output from the MRview screenshot tool), and the desired number ' - 'of overlapping frames.\n') + '(eg. output from the MRview screenshot tool), ' + 'and the desired number of overlapping frames.\n') sys.stderr.write('eg: blend folder1 folder2 20 output_folder\n') sys.exit(1) @@ -38,15 +38,15 @@ if not os.path.exists(OUTPUT_FOLDER): NUM_OUTPUT_FRAMES = len(FILE_LIST_1) + len(FILE_LIST_2) - NUM_OVERLAP for i in range(NUM_OUTPUT_FRAMES): - file_name = 'frame' + '%0*d' % (5, i) + '.png' + file_name = f'frame{i:%05d}.png' if i <= len(FILE_LIST_1) - NUM_OVERLAP: - os.system('cp -L ' + INPUT_FOLDER_1 + '/' + FILE_LIST_1[i] + ' ' + OUTPUT_FOLDER + '/' + file_name) + os.system(f'cp -L {INPUT_FOLDER_1}/{FILE_LIST_1[i]} {OUTPUT_FOLDER}/{file_name}') if len(FILE_LIST_1) - NUM_OVERLAP < i < len(FILE_LIST_1): i2 = i - (len(FILE_LIST_1) - NUM_OVERLAP) - 1 blend_amount = 100 * float(i2 + 1) / float(NUM_OVERLAP) - os.system('convert ' + INPUT_FOLDER_1 + '/' + FILE_LIST_1[i] + ' ' + INPUT_FOLDER_2 \ - + '/' + FILE_LIST_2[i2] + ' -alpha on -compose blend -define compose:args=' \ - + str(blend_amount) + ' -gravity South -composite ' + OUTPUT_FOLDER + '/' + file_name) + os.system(f'convert {INPUT_FOLDER_1}/{FILE_LIST_1[i]} {INPUT_FOLDER_2}/{FILE_LIST_2[i2]} ' + '-alpha on -compose blend ' + f'-define compose:args={blend_amount} -gravity South -composite {OUTPUT_FOLDER}/{file_name}') if i >= (len(FILE_LIST_1)): i2 = i - (len(FILE_LIST_1) - NUM_OVERLAP) - 1 - os.system('cp -L ' + INPUT_FOLDER_2 + '/' + FILE_LIST_2[i2] + ' ' + OUTPUT_FOLDER + '/' + file_name) + os.system(f'cp -L {INPUT_FOLDER_2}/{FILE_LIST_2[i2]} {OUTPUT_FOLDER}/{file_name}') diff --git a/bin/dwi2mask b/bin/dwi2mask index 485a224e51..703575731e 100755 --- a/bin/dwi2mask +++ b/bin/dwi2mask @@ -23,9 +23,10 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Warda Syeda (wtsyeda@unimelb.edu.au)') cmdline.set_synopsis('Generate a binary mask from DWI data') cmdline.add_description('This script serves as an interface for many different algorithms that generate a binary mask from DWI data in different ways. ' - 'Each algorithm available has its own help page, including necessary references; e.g. to see the help page of the \'fslbet\' algorithm, type \'dwi2mask fslbet\'.') + 'Each algorithm available has its own help page, including necessary references; ' + 'e.g. to see the help page of the "fslbet" algorithm, type "dwi2mask fslbet".') cmdline.add_description('More information on mask derivation from DWI data can be found at the following link: \n' - 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') + f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') # General options #common_options = cmdline.add_argument_group('General dwi2mask options') @@ -38,32 +39,25 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import algorithm, app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import algorithm, app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel # Find out which algorithm the user has requested alg = algorithm.get_module(app.ARGS.algorithm) - app.check_output_path(app.ARGS.output) - - input_header = image.Header(path.from_user(app.ARGS.input, False)) + input_header = image.Header(app.ARGS.input) image.check_3d_nonunity(input_header) grad_import_option = app.read_dwgrad_import_options() if not grad_import_option and 'dw_scheme' not in input_header.keyval(): raise MRtrixError('Script requires diffusion gradient table: ' 'either in image header, or using -grad / -fslgrad option') - app.make_scratch_dir() - + app.activate_scratch_dir() # Get input data into the scratch directory - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif') - + ' -strides 0,0,0,1' + grad_import_option, + run.command(f'mrconvert {app.ARGS.input} input.mif -strides 0,0,0,1 {grad_import_option}', preserve_pipes=True) - alg.get_inputs() - - app.goto_scratch_dir() # Generate a mean b=0 image (common task in many algorithms) - if alg.needs_mean_bzero(): + if alg.NEEDS_MEAN_BZERO: run.command('dwiextract input.mif -bzero - | ' 'mrmath - mean - -axis 3 | ' 'mrconvert - bzero.nii -strides +1,+2,+3') @@ -79,6 +73,7 @@ def execute(): #pylint: disable=unused-variable # if the input DWI is volume-contiguous strides = image.Header('input.mif').strides()[0:3] strides = [(abs(value) + 1 - min(abs(v) for v in strides)) * (-1 if value < 0 else 1) for value in strides] + strides = ','.join(map(str, strides)) # From here, the script splits depending on what algorithm is being used # The return value of the execute() function should be the name of the @@ -89,15 +84,9 @@ def execute(): #pylint: disable=unused-variable # the DWI data are valid # (want to ensure that no algorithm includes any voxels where # there is no valid DWI data, regardless of how they operate) - run.command('mrcalc ' - + mask_path - + ' input_pos_mask.mif -mult -' - + ' |' - + ' mrconvert - ' - + path.from_user(app.ARGS.output) - + ' -strides ' + ','.join(str(value) for value in strides) - + ' -datatype bit', - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(f'mrcalc {mask_path} input_pos_mask.mif -mult - | ' + f'mrconvert - {app.ARGS.output} -strides {strides} -datatype bit', + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/bin/dwi2response b/bin/dwi2response index 6dd9f46d5d..94c7b0ca59 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -22,21 +22,39 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Thijs Dhollander (thijs.dhollander@gmail.com)') cmdline.set_synopsis('Estimate response function(s) for spherical deconvolution') - cmdline.add_description('dwi2response offers different algorithms for performing various types of response function estimation. The name of the algorithm must appear as the first argument on the command-line after \'dwi2response\'. The subsequent arguments and options depend on the particular algorithm being invoked.') - cmdline.add_description('Each algorithm available has its own help page, including necessary references; e.g. to see the help page of the \'fa\' algorithm, type \'dwi2response fa\'.') + cmdline.add_description('dwi2response offers different algorithms for performing various types of response function estimation. ' + 'The name of the algorithm must appear as the first argument on the command-line after "dwi2response". ' + 'The subsequent arguments and options depend on the particular algorithm being invoked.') + cmdline.add_description('Each algorithm available has its own help page, including necessary references; ' + 'e.g. to see the help page of the "fa" algorithm, type "dwi2response fa".') cmdline.add_description('More information on response function estimation for spherical deconvolution can be found at the following link: \n' - 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/constrained_spherical_deconvolution/response_function_estimation.html') + f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/constrained_spherical_deconvolution/response_function_estimation.html') cmdline.add_description('Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to ' 'derive an initial voxel exclusion mask. ' 'More information on mask derivation from DWI data can be found at: ' - 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') + f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') # General options common_options = cmdline.add_argument_group('General dwi2response options') - common_options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Provide an initial mask for response voxel selection') - common_options.add_argument('-voxels', type=app.Parser.ImageOut(), metavar='image', help='Output an image showing the final voxel selection(s)') - common_options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='The b-value(s) to use in response function estimation (comma-separated list in case of multiple b-values; b=0 must be included explicitly if desired)') - common_options.add_argument('-lmax', type=app.Parser.SequenceInt(), metavar='values', help='The maximum harmonic degree(s) for response function estimation (comma-separated list in case of multiple b-values)') + common_options.add_argument('-mask', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide an initial mask for response voxel selection') + common_options.add_argument('-voxels', + type=app.Parser.ImageOut(), + metavar='image', + help='Output an image showing the final voxel selection(s)') + common_options.add_argument('-shells', + type=app.Parser.SequenceFloat(), + metavar='bvalues', + help='The b-value(s) to use in response function estimation ' + '(comma-separated list in case of multiple b-values; ' + 'b=0 must be included explicitly if desired)') + common_options.add_argument('-lmax', + type=app.Parser.SequenceInt(), + metavar='values', + help='The maximum harmonic degree(s) for response function estimation ' + '(comma-separated list in case of multiple b-values)') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory @@ -49,60 +67,51 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import CONFIG, MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import algorithm, app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import algorithm, app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel # Find out which algorithm the user has requested alg = algorithm.get_module(app.ARGS.algorithm) - # Check for prior existence of output files, and grab any input files, used by the particular algorithm - if app.ARGS.voxels: - app.check_output_path(app.ARGS.voxels) - alg.check_output_paths() - # Sanitise some inputs, and get ready for data import if app.ARGS.lmax: if any(lmax%2 for lmax in app.ARGS.lmax): raise MRtrixError('Value(s) of lmax must be even') - if alg.needs_single_shell() and not len(app.ARGS.lmax) == 1: + if alg.NEEDS_SINGLE_SHELL and not len(app.ARGS.lmax) == 1: raise MRtrixError('Can only specify a single lmax value for single-shell algorithms') shells_option = '' if app.ARGS.shells: - if alg.needs_single_shell() and len(app.ARGS.shells) != 1: + if alg.NEEDS_SINGLE_SHELL and len(app.ARGS.shells) != 1: raise MRtrixError('Can only specify a single b-value shell for single-shell algorithms') - shells_option = ' -shells ' + ','.join(str(item) for item in app.ARGS.shells) + shells_option = ' -shells ' + ','.join(map(str,app.ARGS.shells)) singleshell_option = '' - if alg.needs_single_shell(): + if alg.NEEDS_SINGLE_SHELL: singleshell_option = ' -singleshell -no_bzero' grad_import_option = app.read_dwgrad_import_options() - if not grad_import_option and 'dw_scheme' not in image.Header(path.from_user(app.ARGS.input, False)).keyval(): - raise MRtrixError('Script requires diffusion gradient table: either in image header, or using -grad / -fslgrad option') - - app.make_scratch_dir() + if not grad_import_option and 'dw_scheme' not in image.Header(app.ARGS.input).keyval(): + raise MRtrixError('Script requires diffusion gradient table: ' + 'either in image header, or using -grad / -fslgrad option') + app.activate_scratch_dir() # Get standard input data into the scratch directory - if alg.needs_single_shell() or shells_option: - app.console('Importing DWI data (' + path.from_user(app.ARGS.input) + ') and selecting b-values...') - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' - -strides 0,0,0,1' + grad_import_option + ' | ' - 'dwiextract - ' + path.to_scratch('dwi.mif') + shells_option + singleshell_option, + if alg.NEEDS_SINGLE_SHELL or shells_option: + app.console(f'Importing DWI data ({app.ARGS.input}) and selecting b-values...') + run.command(f'mrconvert {app.ARGS.input} - -strides 0,0,0,1 {grad_import_option} | ' + f'dwiextract - dwi.mif {shells_option} {singleshell_option}', show=False, preserve_pipes=True) else: # Don't discard b=0 in multi-shell algorithms - app.console('Importing DWI data (' + path.from_user(app.ARGS.input) + ')...') - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('dwi.mif') + ' -strides 0,0,0,1' + grad_import_option, + app.console(f'Importing DWI data ({app.ARGS.input})...') + run.command(f'mrconvert {app.ARGS.input} dwi.mif -strides 0,0,0,1 {grad_import_option}', show=False, preserve_pipes=True) if app.ARGS.mask: - app.console('Importing mask (' + path.from_user(app.ARGS.mask) + ')...') - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + app.console(f'Importing mask ({app.ARGS.mask})...') + run.command(f'mrconvert {app.ARGS.mask} mask.mif -datatype bit', show=False, preserve_pipes=True) - alg.get_inputs() - - app.goto_scratch_dir() - - if alg.supports_mask(): + if alg.SUPPORTS_MASK: if app.ARGS.mask: # Check that the brain mask is appropriate mask_header = image.Header('mask.mif') @@ -111,8 +120,9 @@ def execute(): #pylint: disable=unused-variable if not (len(mask_header.size()) == 3 or (len(mask_header.size()) == 4 and mask_header.size()[3] == 1)): raise MRtrixError('Provided mask image needs to be a 3D image') else: - app.console('Computing brain mask (dwi2mask)...') - run.command('dwi2mask ' + CONFIG['Dwi2maskAlgorithm'] + ' dwi.mif mask.mif', show=False) + dwi2mask_algo = CONFIG['Dwi2maskAlgorithm'] + app.console(f'Computing brain mask (dwi2mask {dwi2mask_algo})...') + run.command(f'dwi2mask {dwi2mask_algo} dwi.mif mask.mif', show=False) if not image.statistics('mask.mif', mask='mask.mif').count: raise MRtrixError(('Provided' if app.ARGS.mask else 'Generated') + ' mask image does not contain any voxels') diff --git a/bin/dwibiascorrect b/bin/dwibiascorrect index 5c0e9f6cbb..910df34216 100755 --- a/bin/dwibiascorrect +++ b/bin/dwibiascorrect @@ -21,13 +21,20 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import algorithm, app, _version #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Perform B1 field inhomogeneity correction for a DWI volume series') - cmdline.add_description('Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to ' + cmdline.add_description('Note that if the -mask command-line option is not specified, ' + 'the MRtrix3 command dwi2mask will automatically be called to ' 'derive a mask that will be passed to the relevant bias field estimation command. ' 'More information on mask derivation from DWI data can be found at the following link: \n' 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') common_options = cmdline.add_argument_group('Options common to all dwibiascorrect algorithms') - common_options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Manually provide an input mask image for bias field estimation') - common_options.add_argument('-bias', type=app.Parser.ImageOut(), metavar='image', help='Output an image containing the estimated bias field') + common_options.add_argument('-mask', + type=app.Parser.ImageIn(), + metavar='image', + help='Manually provide an input mask image for bias field estimation') + common_options.add_argument('-bias', + type=app.Parser.ImageOut(), + metavar='image', + help='Output an image containing the estimated bias field') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory @@ -37,43 +44,38 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import CONFIG, MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import algorithm, app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import algorithm, app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel # Find out which algorithm the user has requested alg = algorithm.get_module(app.ARGS.algorithm) - app.check_output_path(app.ARGS.output) - app.check_output_path(app.ARGS.bias) - alg.check_output_paths() - - app.make_scratch_dir() - + app.activate_scratch_dir() grad_import_option = app.read_dwgrad_import_options() - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif') + grad_import_option, + run.command(f'mrconvert {app.ARGS.input} in.mif {grad_import_option}', preserve_pipes=True) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], preserve_pipes=True) - alg.get_inputs() - - app.goto_scratch_dir() - # Make sure it's actually a DWI that's been passed dwi_header = image.Header('in.mif') if len(dwi_header.size()) != 4: raise MRtrixError('Input image must be a 4D image') if 'dw_scheme' not in dwi_header.keyval(): raise MRtrixError('No valid DW gradient scheme provided or present in image header') - if len(dwi_header.keyval()['dw_scheme']) != dwi_header.size()[3]: - raise MRtrixError('DW gradient scheme contains different number of entries (' + str(len(dwi_header.keyval()['dw_scheme'])) + ' to number of volumes in DWIs (' + dwi_header.size()[3] + ')') + dwscheme_rows = len(dwi_header.keyval()['dw_scheme']) + if dwscheme_rows != dwi_header.size()[3]: + raise MRtrixError(f'DW gradient scheme contains different number of entries' + f' ({dwscheme_rows})' + f' to number of volumes in DWIs' + f' ({dwi_header.size()[3]})') # Generate a brain mask if required, or check the mask if provided by the user if app.ARGS.mask: if not image.match('in.mif', 'mask.mif', up_to_dim=3): raise MRtrixError('Provided mask image does not match input DWI') else: - run.command('dwi2mask ' + CONFIG['Dwi2maskAlgorithm'] + ' in.mif mask.mif') + run.command(['dwi2mask', CONFIG['Dwi2maskAlgorithm'], 'in.mif', 'mask.mif']) # From here, the script splits depending on what estimation algorithm is being used alg.execute() diff --git a/bin/dwibiasnormmask b/bin/dwibiasnormmask index 2d62b3b65e..86762d9e47 100755 --- a/bin/dwibiasnormmask +++ b/bin/dwibiasnormmask @@ -84,58 +84,75 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Dhollander, T.; Tabbara, R.; Rosnarho-Tornstrand, J.; Tournier, J.-D.; Raffelt, D. & Connelly, A. ' 'Multi-tissue log-domain intensity and inhomogeneity normalisation for quantitative apparent fibre density. ' 'In Proc. ISMRM, 2021, 29, 2472') - cmdline.add_argument('input', help='The input DWI series to be corrected') - cmdline.add_argument('output_dwi', help='The output corrected DWI series') - cmdline.add_argument('output_mask', help='The output DWI mask') + cmdline.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series to be corrected') + cmdline.add_argument('output_dwi', + type=app.Parser.ImageOut(), + help='The output corrected DWI series') + cmdline.add_argument('output_mask', + type=app.Parser.ImageOut(), + help='The output DWI mask') output_options = cmdline.add_argument_group('Options that modulate the outputs of the script') - output_options.add_argument('-output_bias', metavar='image', + output_options.add_argument('-output_bias', + type=app.Parser.ImageOut(), help='Export the final estimated bias field to an image') - output_options.add_argument('-output_scale', metavar='file', + output_options.add_argument('-output_scale', + type=app.Parser.FileOut(), help='Write the scaling factor applied to the DWI series to a text file') - output_options.add_argument('-output_tissuesum', metavar='image', + output_options.add_argument('-output_tissuesum', + type=app.Parser.ImageOut(), help='Export the tissue sum image that was used to generate the final mask') - output_options.add_argument('-reference', type=app.Parser.Float(0.0), metavar='value', default=REFERENCE_INTENSITY, - help='Set the target CSF b=0 intensity in the output DWI series (default: ' + str(REFERENCE_INTENSITY) + ')') + output_options.add_argument('-reference', + type=app.Parser.Float(0.0), + metavar='value', + default=REFERENCE_INTENSITY, + help=f'Set the target CSF b=0 intensity in the output DWI series (default: {REFERENCE_INTENSITY})') internal_options = cmdline.add_argument_group('Options relevant to the internal optimisation procedure') - internal_options.add_argument('-dice', type=app.Parser.Float(0.0, 1.0), default=DICE_COEFF_DEFAULT, metavar='value', - help='Set the Dice coefficient threshold for similarity of masks between sequential iterations that will ' - 'result in termination due to convergence; default = ' + str(DICE_COEFF_DEFAULT)) - internal_options.add_argument('-init_mask', metavar='image', + internal_options.add_argument('-dice', + type=app.Parser.Float(0.0, 1.0), + default=DICE_COEFF_DEFAULT, + metavar='value', + help=f'Set the Dice coefficient threshold for similarity of masks between sequential iterations that will ' + f'result in termination due to convergence; default = {DICE_COEFF_DEFAULT}') + internal_options.add_argument('-init_mask', + type=app.Parser.ImageIn(), help='Provide an initial mask for the first iteration of the algorithm ' '(if not provided, the default dwi2mask algorithm will be used)') - internal_options.add_argument('-max_iters', type=app.Parser.Int(0), default=DWIBIASCORRECT_MAX_ITERS, metavar='count', - help='The maximum number of iterations (see Description); default is ' + str(DWIBIASCORRECT_MAX_ITERS) + '; ' - 'set to 0 to proceed until convergence') - internal_options.add_argument('-mask_algo', choices=MASK_ALGOS, metavar='algorithm', - help='The algorithm to use for mask estimation, potentially based on the ODF sum image (see Description); default: ' + MASK_ALGO_DEFAULT) - internal_options.add_argument('-lmax', metavar='values', type=app.Parser.SequenceInt(), + internal_options.add_argument('-max_iters', + type=app.Parser.Int(0), + default=DWIBIASCORRECT_MAX_ITERS, + metavar='count', + help=f'The maximum number of iterations (see Description); default is {DWIBIASCORRECT_MAX_ITERS}; ' + f'set to 0 to proceed until convergence') + internal_options.add_argument('-mask_algo', + choices=MASK_ALGOS, + metavar='algorithm', + help=f'The algorithm to use for mask estimation, ' + f'potentially based on the ODF sum image (see Description); ' + f'default: {MASK_ALGO_DEFAULT}') + internal_options.add_argument('-lmax', + metavar='values', + type=app.Parser.SequenceInt(), help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' - 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') + f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' + f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data)') app.add_dwgrad_import_options(cmdline) def execute(): #pylint: disable=unused-variable from mrtrix3 import CONFIG, MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import app, fsl, image, matrix, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import app, fsl, image, matrix, run #pylint: disable=no-name-in-module, import-outside-toplevel # Check user inputs - if app.ARGS.max_iters < 0: - raise MRtrixError('Maximum number of iterations must be a non-negative integer') lmax = None if app.ARGS.lmax: - try: - lmax = [int(i) for i in app.ARGS.lmax.split(',')] - except ValueError as exc: - raise MRtrixError('Values provided to -lmax option must be a comma-separated list of integers') from exc + lmax = app.ARGS.lmax if any(value < 0 or value % 2 for value in lmax): raise MRtrixError('lmax values must be non-negative even integers') if len(lmax) not in [2, 3]: raise MRtrixError('Length of lmax vector expected to be either 2 or 3') - if app.ARGS.dice <= 0.0 or app.ARGS.dice > 1.0: - raise MRtrixError('Dice coefficient for convergence detection must lie in the range (0.0, 1.0]') - if app.ARGS.reference <= 0.0: - raise MRtrixError('Reference intensity must be positive') # Check what masking agorithm is going to be used mask_algo = MASK_ALGO_DEFAULT @@ -144,29 +161,22 @@ def execute(): #pylint: disable=unused-variable elif 'DwibiasnormmaskMaskAlgorithm' in CONFIG: mask_algo = CONFIG['DwibiasnormmaskMaskAlgorithm'] if not mask_algo in MASK_ALGOS: - raise MRtrixError('Invalid masking algorithm selection "%s" in MRtrix config file' % mask_algo) - app.console('"%s" algorithm will be used for brain masking during iteration as specified in config file' % mask_algo) + raise MRtrixError(f'Invalid masking algorithm selection "{mask_algo}" in MRtrix config file') + app.console(f'"{mask_algo}" algorithm will be used for brain masking during iteration as specified in config file') else: - app.console('Default "%s" algorithm will be used for brain masking during iteration' % MASK_ALGO_DEFAULT) + app.console(f'Default "{MASK_ALGO_DEFAULT}" algorithm will be used for brain masking during iteration') # Check mask algorithm, including availability of external software if necessary for mask_algo, software, command in [('fslbet', 'FSL', 'bet'), ('hdbet', 'HD-BET', 'hd-bet'), ('synthstrip', 'FreeSurfer', 'mri_synthstrip')]: if app.ARGS.mask_algo == mask_algo and not shutil.which(command): - raise MRtrixError(software + ' command "' + command + '" not found; cannot use for internal mask calculations') - - app.check_output_path(app.ARGS.output_dwi) - app.check_output_path(app.ARGS.output_mask) - app.make_scratch_dir() + raise MRtrixError(f'{software} command "{command}" not found; cannot use for internal mask calculations') + app.activate_scratch_dir() grad_import_option = app.read_dwgrad_import_options() - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' - + path.to_scratch('input.mif') + grad_import_option) - + run.command(f'mrconvert {app.ARGS.input} input.mif {grad_import_option}') if app.ARGS.init_mask: - run.command('mrconvert ' + path.from_user(app.ARGS.init_mask) + ' ' - + path.to_scratch('dwi_mask_init.mif') + ' -datatype bit') + run.command(f'mrconvert {app.ARGS.init_mask} dwi_mask_init.mif -datatype bit') - app.goto_scratch_dir() # Check inputs @@ -176,10 +186,12 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Input image must be a 4D image') if 'dw_scheme' not in dwi_header.keyval(): raise MRtrixError('No valid DW gradient scheme provided or present in image header') - if len(dwi_header.keyval()['dw_scheme']) != dwi_header.size()[3]: - raise MRtrixError('DW gradient scheme contains different number of entries (' - + str(len(dwi_header.keyval()['dw_scheme'])) - + ' to number of volumes in DWIs (' + dwi_header.size()[3] + ')') + dwscheme_rows = len(dwi_header.keyval()['dw_scheme']) + if dwscheme_rows != dwi_header.size()[3]: + raise MRtrixError(f'DW gradient scheme contains different number of entries' + f' ({dwscheme_rows})' + f' to number of volumes in DWIs' + f' ({dwi_header.size()[3]})') # Determine whether we are working with single-shell or multi-shell data bvalues = [ @@ -190,7 +202,9 @@ def execute(): #pylint: disable=unused-variable if lmax is None: lmax = LMAXES_MULTI if multishell else LMAXES_SINGLE elif len(lmax) == 3 and not multishell: - raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, but input DWI is not multi-shell') + raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, ' + 'but input DWI is not multi-shell') + lmax_option = ' -lmax ' + ','.join(map(str, lmax)) # Create a mask of voxels where the input data contain positive values; # we want to make sure that these never end up included in the output mask @@ -203,9 +217,7 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Provided mask image does not match input DWI') else: app.debug('Performing intial DWI brain masking') - run.command('dwi2mask ' - + CONFIG['Dwi2maskAlgorithm'] - + ' input.mif dwi_mask_init.mif') + run.command(['dwi2mask', CONFIG['Dwi2maskAlgorithm'], 'input.mif', 'dwi_mask_init.mif']) # Combined RF estimation / CSD / mtnormalise / mask revision class Tissue(object): #pylint: disable=useless-object-inheritance @@ -216,9 +228,11 @@ def execute(): #pylint: disable=unused-variable self.fod_init = 'FODinit_' + name + iter_string + '.mif' self.fod_norm = 'FODnorm_' + name + iter_string + '.mif' - - app.debug('Commencing iterative DWI bias field correction and brain masking with ' - + ('a maximum of ' + str(app.ARGS.max_iters) if app.ARGS.max_iters else 'no limit on number of ') + ' iterations') + iteration_message = f'a maximum of {app.ARGS.max_iters}' \ + if app.ARGS.max_iters \ + else 'no limit on number of ' + app.debug('Commencing iterative DWI bias field correction and brain masking ' + f'with {iteration_message} iterations') dwi_image = 'input.mif' dwi_mask_image = 'dwi_mask_init.mif' @@ -230,8 +244,7 @@ def execute(): #pylint: disable=unused-variable total_scaling_factor = 1.0 def msg(): - return 'Iteration {0}; {1} step; previous Dice coefficient {2}' \ - .format(iteration, step, prev_dice_coefficient) + return f'Iteration {iteration}; {step} step; previous Dice coefficient {prev_dice_coefficient}' progress = app.ProgressBar(msg) iteration = 1 @@ -243,11 +256,7 @@ def execute(): #pylint: disable=unused-variable step = 'dwi2response' progress.increment() - run.command('dwi2response dhollander ' - + dwi_image - + ' -mask ' - + dwi_mask_image - + ' ' + run.command(f'dwi2response dhollander {dwi_image} -mask {dwi_mask_image} ' + ' '.join(tissue.tissue_rf for tissue in tissues)) @@ -258,18 +267,14 @@ def execute(): #pylint: disable=unused-variable step = 'dwi2fod' progress.increment() - app.debug('Performing CSD with lmax values: ' + ','.join(str(item) for item in lmax)) - run.command('dwi2fod msmt_csd ' - + dwi_image - + ' -lmax ' + ','.join(str(item) for item in lmax) - + ' ' - + ' '.join(tissue.tissue_rf + ' ' + tissue.fod_init - for tissue in tissues)) + app.debug('Performing CSD with lmax values: ' + ','.join(map(str, lmax))) + run.command(f'dwi2fod msmt_csd {dwi_image} {lmax_option} ' + + ' '.join(tissue.tissue_rf + ' ' + tissue.fod_init for tissue in tissues)) step = 'maskfilter' progress.increment() eroded_mask = os.path.splitext(dwi_mask_image)[0] + '_eroded.mif' - run.command('maskfilter ' + dwi_mask_image + ' erode ' + eroded_mask) + run.command(f'maskfilter {dwi_mask_image} erode {eroded_mask}') step = 'mtnormalise' progress.increment() @@ -277,18 +282,13 @@ def execute(): #pylint: disable=unused-variable bias_field_image = 'field' + iter_string + '.mif' factors_path = 'factors' + iter_string + '.txt' - run.command('mtnormalise -balanced' - + ' -mask ' + eroded_mask - + ' -check_norm ' + bias_field_image - + ' -check_factors ' + factors_path - + ' ' + run.command(f'mtnormalise -balanced -mask {eroded_mask} -check_norm {bias_field_image} -check_factors {factors_path} ' + ' '.join(tissue.fod_init + ' ' + tissue.fod_norm for tissue in tissues)) app.cleanup([tissue.fod_init for tissue in tissues]) app.cleanup(eroded_mask) - app.debug('Iteration ' + str(iteration) + ', ' - + 'applying estimated bias field and appropiate scaling factor...') + app.debug(f'Iteration {iteration}, applying estimated bias field and appropiate scaling factor...') csf_rf = matrix.load_matrix(tissues[-1].tissue_rf) csf_rf_bzero_lzero = csf_rf[0][0] app.cleanup([tissue.tissue_rf for tissue in tissues]) @@ -297,74 +297,60 @@ def execute(): #pylint: disable=unused-variable app.cleanup(factors_path) scale_multiplier = (app.ARGS.reference * math.sqrt(4.0*math.pi)) / \ (csf_rf_bzero_lzero / csf_balance_factor) - new_dwi_image = 'dwi' + iter_string + '.mif' - run.command('mrcalc ' + dwi_image + ' ' - + bias_field_image + ' -div ' - + str(scale_multiplier) + ' -mult ' - + new_dwi_image) + new_dwi_image = f'dwi{iter_string}.mif' + run.command(f'mrcalc {dwi_image} {bias_field_image} -div {scale_multiplier} -mult {new_dwi_image}') old_dwi_image = dwi_image dwi_image = new_dwi_image old_tissue_sum_image = tissue_sum_image - tissue_sum_image = 'tissue_sum' + iter_string + '.mif' + tissue_sum_image = f'tissue_sum{iter_string}.mif' - app.debug('Iteration ' + str(iteration) + ', ' - + 'revising brain mask...') + app.debug(f'Iteration {iteration}, revising brain mask...') step = 'masking' progress.increment() - run.command('mrconvert ' - + tissues[0].fod_norm - + ' -coord 3 0 - |' - + ' mrmath - ' - + ' '.join(tissue.fod_norm for tissue in tissues[1:]) - + ' sum - | ' - + 'mrcalc - ' + str(math.sqrt(4.0 * math.pi)) + ' -mult ' - + tissue_sum_image) + run.command(f'mrconvert {tissues[0].fod_norm} -coord 3 0 - | ' \ + f'mrmath - {" ".join(tissue.fod_norm for tissue in tissues[1:])} sum - | ' + f'mrcalc - {math.sqrt(4.0 * math.pi)} -mult {tissue_sum_image}') app.cleanup([tissue.fod_norm for tissue in tissues]) - new_dwi_mask_image = 'dwi_mask' + iter_string + '.mif' + new_dwi_mask_image = f'dwi_mask{iter_string}.mif' tissue_sum_image_nii = None new_dwi_mask_image_nii = None if mask_algo in ['fslbet', 'hdbet', 'synthstrip']: - tissue_sum_image_nii = os.path.splitext(tissue_sum_image)[0] + '.nii' - run.command('mrconvert ' + tissue_sum_image + ' ' + tissue_sum_image_nii) - new_dwi_mask_image_nii = os.path.splitext(new_dwi_mask_image)[0] + '.nii' + tissue_sum_image_nii = f'{os.path.splitext(tissue_sum_image)[0]}.nii' + run.command(f'mrconvert {tissue_sum_image} {tissue_sum_image_nii}') + new_dwi_mask_image_nii = f'{os.path.splitext(new_dwi_mask_image)[0]}.nii' if mask_algo == 'dwi2mask': - run.command('dwi2mask ' + CONFIG.get('Dwi2maskAlgorithm', 'legacy') + ' ' + new_dwi_image + ' ' + new_dwi_mask_image) + run.command(['dwi2mask', CONFIG.get('Dwi2maskAlgorithm', 'legacy'), new_dwi_image, new_dwi_mask_image]) elif mask_algo == 'fslbet': - run.command('bet ' + tissue_sum_image_nii + ' ' + new_dwi_mask_image_nii + ' -R -m') + run.command(f'bet {tissue_sum_image_nii} {new_dwi_mask_image_nii} -R -m') app.cleanup(fsl.find_image(os.path.splitext(new_dwi_mask_image_nii)[0])) new_dwi_mask_image_nii = fsl.find_image(os.path.splitext(new_dwi_mask_image_nii)[0] + '_mask') - run.command('mrcalc ' + new_dwi_mask_image_nii + ' input_pos_mask.mif -mult ' + new_dwi_mask_image) + run.command(f'mrcalc {new_dwi_mask_image_nii} input_pos_mask.mif -mult {new_dwi_mask_image}') elif mask_algo == 'hdbet': try: - run.command('hd-bet -i ' + tissue_sum_image_nii) + run.command(f'hd-bet -i {tissue_sum_image_nii}') except run.MRtrixCmdError as e_gpu: try: - run.command('hd-bet -i ' + tissue_sum_image_nii + ' -device cpu -mode fast -tta 0') + run.command(f'hd-bet -i {tissue_sum_image_nii} -device cpu -mode fast -tta 0') except run.MRtrixCmdError as e_cpu: raise run.MRtrixCmdError('hd-bet', 1, e_gpu.stdout + e_cpu.stdout, e_gpu.stderr + e_cpu.stderr) new_dwi_mask_image_nii = os.path.splitext(tissue_sum_image)[0] + '_bet_mask.nii.gz' - run.command('mrcalc ' + new_dwi_mask_image_nii + ' input_pos_mask.mif -mult ' + new_dwi_mask_image) + run.command(f'mrcalc {new_dwi_mask_image_nii} input_pos_mask.mif -mult {new_dwi_mask_image}') elif mask_algo in ['mrthreshold', 'threshold']: mrthreshold_abs_option = ' -abs 0.5' if mask_algo == 'threshold' else '' - run.command('mrthreshold ' - + tissue_sum_image - + mrthreshold_abs_option - + ' - |' - + ' maskfilter - connect -largest - |' - + ' mrcalc 1 - -sub - -datatype bit |' - + ' maskfilter - connect -largest - |' - + ' mrcalc 1 - -sub - -datatype bit |' - + ' maskfilter - clean - |' - + ' mrcalc - input_pos_mask.mif -mult ' - + new_dwi_mask_image - + ' -datatype bit') + run.command(f'mrthreshold {tissue_sum_image} {mrthreshold_abs_option} - |' + f' maskfilter - connect -largest - |' + f' mrcalc 1 - -sub - -datatype bit |' + f' maskfilter - connect -largest - |' + f' mrcalc 1 - -sub - -datatype bit |' + f' maskfilter - clean - |' + f' mrcalc - input_pos_mask.mif -mult {new_dwi_mask_image} -datatype bit') elif mask_algo == 'synthstrip': - run.command('mri_synthstrip -i ' + tissue_sum_image_nii + ' --mask ' + new_dwi_mask_image_nii) - run.command('mrcalc ' + new_dwi_mask_image_nii + ' input_pos_mask.mif -mult ' + new_dwi_mask_image) + run.command(f'mri_synthstrip -i {tissue_sum_image_nii} --mask {new_dwi_mask_image_nii}') + run.command(f'mrcalc {new_dwi_mask_image_nii} input_pos_mask.mif -mult {new_dwi_mask_image}') else: assert False if tissue_sum_image_nii: @@ -378,24 +364,25 @@ def execute(): #pylint: disable=unused-variable mask=dwi_mask_image).count dwi_new_mask_count = image.statistics(new_dwi_mask_image, mask=new_dwi_mask_image).count - app.debug('Old mask voxel count: ' + str(dwi_old_mask_count)) - app.debug('New mask voxel count: ' + str(dwi_new_mask_count)) - dwi_mask_overlap_image = 'dwi_mask_overlap' + iter_string + '.mif' - run.command(['mrcalc', dwi_mask_image, new_dwi_mask_image, '-mult', dwi_mask_overlap_image]) + app.debug(f'Old mask voxel count: {dwi_old_mask_count}') + app.debug(f'New mask voxel count: {dwi_new_mask_count}') + dwi_mask_overlap_image = f'dwi_mask_overlap{iter_string}.mif' + run.command(f'mrcalc {dwi_mask_image} {new_dwi_mask_image} -mult {dwi_mask_overlap_image}') old_dwi_mask_image = dwi_mask_image dwi_mask_image = new_dwi_mask_image mask_overlap_count = image.statistics(dwi_mask_overlap_image, mask=dwi_mask_overlap_image).count - app.debug('Mask overlap voxel count: ' + str(mask_overlap_count)) + app.debug(f'Mask overlap voxel count: {mask_overlap_count}') new_dice_coefficient = 2.0 * mask_overlap_count / \ (dwi_old_mask_count + dwi_new_mask_count) if iteration == app.ARGS.max_iters: progress.done() - app.console('Terminating due to reaching maximum %d iterations; final Dice coefficient = %f' % (iteration, new_dice_coefficient)) + app.console(f'Terminating due to reaching maximum {iteration} iterations; ' + f'final Dice coefficient = {new_dice_coefficient}') app.cleanup(old_dwi_image) app.cleanup(old_dwi_mask_image) app.cleanup(old_bias_field_image) @@ -405,7 +392,8 @@ def execute(): #pylint: disable=unused-variable if new_dice_coefficient > app.ARGS.dice: progress.done() - app.console('Exiting loop after %d iterations due to mask convergence (Dice coefficient = %f)' % (iteration, new_dice_coefficient)) + app.console(f'Exiting loop after {iteration} iterations due to mask convergence ' + f'(Dice coefficient = {new_dice_coefficient})') app.cleanup(old_dwi_image) app.cleanup(old_dwi_mask_image) app.cleanup(old_bias_field_image) @@ -415,8 +403,9 @@ def execute(): #pylint: disable=unused-variable if new_dice_coefficient < prev_dice_coefficient: progress.done() - app.warn('Mask divergence at iteration %d (Dice coefficient = %f); ' % (iteration, new_dice_coefficient) - + ' using mask from previous iteration') + app.warn(f'Mask divergence at iteration {iteration} ' + f'(Dice coefficient = {new_dice_coefficient}); ' + f' using mask from previous iteration') app.cleanup(dwi_image) app.cleanup(dwi_mask_image) app.cleanup(bias_field_image) @@ -431,28 +420,28 @@ def execute(): #pylint: disable=unused-variable prev_dice_coefficient = new_dice_coefficient - run.command(['mrconvert', dwi_image, path.from_user(app.ARGS.output_dwi, False)], - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', dwi_image, app.ARGS.output_dwi], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) if app.ARGS.output_bias: - run.command(['mrconvert', bias_field_image, path.from_user(app.ARGS.output_bias, False)], - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', bias_field_image, app.ARGS.output_bias], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) if app.ARGS.output_mask: - run.command(['mrconvert', dwi_mask_image, path.from_user(app.ARGS.output_mask, False)], - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', dwi_mask_image, app.ARGS.output_mask], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) if app.ARGS.output_scale: - matrix.save_vector(path.from_user(app.ARGS.output_scale, False), + matrix.save_vector(app.ARGS.output_scale, [total_scaling_factor], force=app.FORCE_OVERWRITE) if app.ARGS.output_tissuesum: - run.command(['mrconvert', tissue_sum_image, path.from_user(app.ARGS.output_tissuesum, False)], - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', tissue_sum_image, app.ARGS.output_tissuesum], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) diff --git a/bin/dwicat b/bin/dwicat index d920818376..e7ede9a896 100755 --- a/bin/dwicat +++ b/bin/dwicat @@ -24,22 +24,32 @@ import json, shutil def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Lena Dorfschmidt (ld548@cam.ac.uk) and Jakub Vohryzek (jakub.vohryzek@queens.ox.ac.uk) and Robert E. Smith (robert.smith@florey.edu.au)') + cmdline.set_author('Lena Dorfschmidt (ld548@cam.ac.uk) ' + 'and Jakub Vohryzek (jakub.vohryzek@queens.ox.ac.uk) ' + 'and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Concatenating multiple DWI series accounting for differential intensity scaling') cmdline.add_description('This script concatenates two or more 4D DWI series, accounting for the ' 'fact that there may be differences in intensity scaling between those series. ' 'This intensity scaling is corrected by determining scaling factors that will ' 'make the overall image intensities in the b=0 volumes of each series approximately ' 'equivalent.') - cmdline.add_argument('inputs', nargs='+', type=app.Parser.ImageIn(), help='Multiple input diffusion MRI series') - cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output image series (all DWIs concatenated)') - cmdline.add_argument('-mask', metavar='image', type=app.Parser.ImageIn(), help='Provide a binary mask within which image intensities will be matched') + cmdline.add_argument('inputs', + nargs='+', + type=app.Parser.ImageIn(), + help='Multiple input diffusion MRI series') + cmdline.add_argument('output', + type=app.Parser.ImageOut(), + help='The output image series (all DWIs concatenated)') + cmdline.add_argument('-mask', + metavar='image', + type=app.Parser.ImageIn(), + help='Provide a binary mask within which image intensities will be matched') def execute(): #pylint: disable=unused-variable from mrtrix3 import CONFIG, MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel num_inputs = len(app.ARGS.inputs) if num_inputs < 2: @@ -48,9 +58,9 @@ def execute(): #pylint: disable=unused-variable # check input data def check_header(header): if len(header.size()) > 4: - raise MRtrixError('Image "' + header.name() + '" contains more than 4 dimensions') + raise MRtrixError(f'Image "{header.name()}" contains more than 4 dimensions') if not 'dw_scheme' in header.keyval(): - raise MRtrixError('Image "' + header.name() + '" does not contain a gradient table') + raise MRtrixError(f'Image "{header.name()}" does not contain a gradient table') dw_scheme = header.keyval()['dw_scheme'] try: if isinstance(dw_scheme[0], list): @@ -60,22 +70,24 @@ def execute(): #pylint: disable=unused-variable else: raise MRtrixError except (IndexError, MRtrixError): - raise MRtrixError('Image "' + header.name() + '" contains gradient table of unknown format') # pylint: disable=raise-missing-from + raise MRtrixError(f'Image "{header.name()}" contains gradient table of unknown format') # pylint: disable=raise-missing-from if len(header.size()) == 4: num_volumes = header.size()[3] if num_grad_lines != num_volumes: - raise MRtrixError('Number of lines in gradient table for image "' + header.name() + '" (' + str(num_grad_lines) + ') does not match number of volumes (' + str(num_volumes) + ')') + raise MRtrixError(f'Number of lines in gradient table for image "{header.name()}" ({num_grad_lines}) ' + f'does not match number of volumes ({num_volumes})') elif not (num_grad_lines == 1 and len(dw_scheme) >= 4 and dw_scheme[3] <= float(CONFIG.get('BZeroThreshold', 10.0))): - raise MRtrixError('Image "' + header.name() + '" is 3D, and cannot be validated as a b=0 volume') + raise MRtrixError(f'Image "{header.name()}" is 3D, and cannot be validated as a b=0 volume') - first_header = image.Header(path.from_user(app.ARGS.inputs[0], False)) + first_header = image.Header(app.ARGS.inputs[0]) check_header(first_header) warn_protocol_mismatch = False for filename in app.ARGS.inputs[1:]: - this_header = image.Header(path.from_user(filename, False)) + this_header = image.Header(filename) check_header(this_header) if this_header.size()[0:3] != first_header.size()[0:3]: - raise MRtrixError('Spatial dimensions of image "' + filename + '" do not match those of first image "' + first_header.name() + '"') + raise MRtrixError(f'Spatial dimensions of image "{filename}" ' + f'do not match those of first image "{first_header.name()}"') for field_name in [ 'EchoTime', 'RepetitionTime', 'FlipAngle' ]: first_value = first_header.keyval().get(field_name) this_value = this_header.keyval().get(field_name) @@ -85,29 +97,25 @@ def execute(): #pylint: disable=unused-variable app.warn('Mismatched protocol acquisition parameters detected between input images; ' + \ 'the assumption of equivalent intensities between b=0 volumes of different inputs underlying operation of this script may not be valid') if app.ARGS.mask: - mask_header = image.Header(path.from_user(app.ARGS.mask, False)) + mask_header = image.Header(app.ARGS.mask) if mask_header.size()[0:3] != first_header.size()[0:3]: - raise MRtrixError('Spatial dimensions of mask image "' + app.ARGS.mask + '" do not match those of first image "' + first_header.name() + '"') - - # check output path - app.check_output_path(path.from_user(app.ARGS.output, False)) + raise MRtrixError(f'Spatial dimensions of mask image "{app.ARGS.mask}" do not match those of first image "{first_header.name()}"') # import data to scratch directory - app.make_scratch_dir() + app.activate_scratch_dir() for index, filename in enumerate(app.ARGS.inputs): - run.command('mrconvert ' + path.from_user(filename) + ' ' + path.to_scratch(str(index) + 'in.mif'), + run.command(['mrconvert', filename, f'{index}in.mif'], preserve_pipes=True) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], preserve_pipes=True) - app.goto_scratch_dir() # extract b=0 volumes within each input series for index in range(0, num_inputs): - infile = str(index) + 'in.mif' - outfile = str(index) + 'b0.mif' + infile = f'{index}in.mif' + outfile = f'{index}b0.mif' if len(image.Header(infile).size()) > 3: - run.command('dwiextract ' + infile + ' ' + outfile + ' -bzero') + run.command(f'dwiextract {infile} {outfile} -bzero') else: run.function(shutil.copyfile, infile, outfile) @@ -125,7 +133,7 @@ def execute(): #pylint: disable=unused-variable # based on the resulting matrix of optimal scaling factors filelist = [ '0in.mif' ] for index in range(1, num_inputs): - stderr_text = run.command('mrhistmatch scale ' + str(index) + 'b0.mif 0b0.mif ' + str(index) + 'rescaledb0.mif' + mask_option).stderr + stderr_text = run.command(f'mrhistmatch scale {index}b0.mif 0b0.mif {index}rescaledb0.mif {mask_option}').stderr scaling_factor = None for line in stderr_text.splitlines(): if 'Estimated scale factor is' in line: @@ -136,13 +144,13 @@ def execute(): #pylint: disable=unused-variable break if scaling_factor is None: raise MRtrixError('Unable to extract scaling factor from mrhistmatch output') - filename = str(index) + 'rescaled.mif' - run.command('mrcalc ' + str(index) + 'in.mif ' + str(scaling_factor) + ' -mult ' + filename) + filename = f'{index}rescaled.mif' + run.command(f'mrcalc {index}in.mif {scaling_factor} -mult {filename}') filelist.append(filename) # concatenate all series together - run.command('mrcat ' + ' '.join(filelist) + ' - -axis 3 | ' + \ - 'mrconvert - result.mif -json_export result_init.json -strides 0,0,0,1') + run.command(['mrcat', filelist, '-', '-axis', '3', '|', + 'mrconvert', '-', 'result.mif', '-json_export', 'result_init.json', '-strides', '0,0,0,1']) # remove current contents of command_history, since there's no sensible # way to choose from which input image the contents should be taken; @@ -153,7 +161,7 @@ def execute(): #pylint: disable=unused-variable with open('result_final.json', 'w', encoding='utf-8') as output_json_file: json.dump(keyval, output_json_file) - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), + run.command(['mrconvert', 'result.mif', app.ARGS.output], mrconvert_keyval='result_final.json', force=app.FORCE_OVERWRITE) diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index baa83fe169..f600e8af70 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -24,61 +24,216 @@ import glob, itertools, json, math, os, shutil, sys, shlex def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app, _version #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - cmdline.set_synopsis('Perform diffusion image pre-processing using FSL\'s eddy tool; including inhomogeneity distortion correction using FSL\'s topup tool if possible') - cmdline.add_description('This script is intended to provide convenience of use of the FSL software tools topup and eddy for performing DWI pre-processing, by encapsulating some of the surrounding image data and metadata processing steps. It is intended to simply these processing steps for most commonly-used DWI acquisition strategies, whilst also providing support for some more exotic acquisitions. The "example usage" section demonstrates the ways in which the script can be used based on the (compulsory) -rpe_* command-line options.') - cmdline.add_description('More information on use of the dwifslpreproc command can be found at the following link: \nhttps://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/dwifslpreproc.html') - cmdline.add_description('Note that the MRtrix3 command dwi2mask will automatically be called to derive a processing mask for the FSL command "eddy", which determines which voxels contribute to the estimation of geometric distortion parameters and possibly also the classification of outlier slices. If FSL command "topup" is used to estimate a susceptibility field, then dwi2mask will be executed on the resuts of running FSL command "applytopup" to the input DWIs; otherwise it will be executed directly on the input DWIs. Alternatively, the -eddy_mask option can be specified in order to manually provide such a processing mask. More information on mask derivation from DWI data can be found at: https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') - cmdline.add_description('The "-topup_options" and "-eddy_options" command-line options allow the user to pass desired command-line options directly to the FSL commands topup and eddy. The available options for those commands may vary between versions of FSL; users can interrogate such by querying the help pages of the installed software, and/or the FSL online documentation: (topup) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide ; (eddy) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide') - cmdline.add_description('The script will attempt to run the CUDA version of eddy; if this does not succeed for any reason, or is not present on the system, the CPU version will be attempted instead. By default, the CUDA eddy binary found that indicates compilation against the most recent version of CUDA will be attempted; this can be over-ridden by providing a soft-link "eddy_cuda" within your path that links to the binary you wish to be executed.') - cmdline.add_description('Note that this script does not perform any explicit registration between images provided to topup via the -se_epi option, and the DWI volumes provided to eddy. In some instances (motion between acquisitions) this can result in erroneous application of the inhomogeneity field during distortion correction. Use of the -align_seepi option is advocated in this scenario, which ensures that the first volume in the series provided to topup is also the first volume in the series provided to eddy, guaranteeing alignment. But a prerequisite for this approach is that the image contrast within the images provided to the -se_epi option must match the b=0 volumes present within the input DWI series: this means equivalent TE, TR and flip angle (note that differences in multi-band factors between two acquisitions may lead to differences in TR).') + cmdline.set_synopsis('Perform diffusion image pre-processing using FSL\'s eddy tool; ' + 'including inhomogeneity distortion correction using FSL\'s topup tool if possible') + cmdline.add_description('This script is intended to provide convenience of use of the FSL software tools topup and eddy for performing DWI pre-processing, ' + 'by encapsulating some of the surrounding image data and metadata processing steps. ' + 'It is intended to simply these processing steps for most commonly-used DWI acquisition strategies, ' + 'whilst also providing support for some more exotic acquisitions. ' + 'The "example usage" section demonstrates the ways in which the script can be used based on the (compulsory) -rpe_* command-line options.') + cmdline.add_description('More information on use of the dwifslpreproc command can be found at the following link: \n' + f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/dwifslpreproc.html') + cmdline.add_description('Note that the MRtrix3 command dwi2mask will automatically be called to derive a processing mask for the FSL command "eddy", ' + 'which determines which voxels contribute to the estimation of geometric distortion parameters and possibly also the classification of outlier slices. ' + 'If FSL command "topup" is used to estimate a susceptibility field, ' + 'then dwi2mask will be executed on the resuts of running FSL command "applytopup" to the input DWIs; ' + 'otherwise it will be executed directly on the input DWIs. ' + 'Alternatively, the -eddy_mask option can be specified in order to manually provide such a processing mask. ' + 'More information on mask derivation from DWI data can be found at: ' + f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') + cmdline.add_description('The "-topup_options" and "-eddy_options" command-line options allow the user to pass desired command-line options directly to the FSL commands topup and eddy. ' + 'The available options for those commands may vary between versions of FSL; ' + 'users can interrogate such by querying the help pages of the installed software, ' + 'and/or the FSL online documentation: ' + '(topup) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide ; ' + '(eddy) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide') + cmdline.add_description('The script will attempt to run the CUDA version of eddy; ' + 'if this does not succeed for any reason, or is not present on the system, ' + 'the CPU version will be attempted instead. ' + 'By default, the CUDA eddy binary found that indicates compilation against the most recent version of CUDA will be attempted; ' + 'this can be over-ridden by providing a soft-link "eddy_cuda" within your path that links to the binary you wish to be executed.') + cmdline.add_description('Note that this script does not perform any explicit registration between images provided to topup via the -se_epi option, ' + 'and the DWI volumes provided to eddy. ' + 'In some instances (motion between acquisitions) this can result in erroneous application of the inhomogeneity field during distortion correction. ' + 'Use of the -align_seepi option is advocated in this scenario, ' + 'which ensures that the first volume in the series provided to topup is also the first volume in the series provided to eddy, ' + 'guaranteeing alignment. ' + 'But a prerequisite for this approach is that the image contrast within the images provided to the -se_epi option ' + 'must match the b=0 volumes present within the input DWI series: ' + 'this means equivalent TE, TR and flip angle ' + '(note that differences in multi-band factors between two acquisitions may lead to differences in TR).') cmdline.add_example_usage('A basic DWI acquisition, where all image volumes are acquired in a single protocol with fixed phase encoding', 'dwifslpreproc DWI_in.mif DWI_out.mif -rpe_none -pe_dir ap -readout_time 0.55', 'Due to use of a single fixed phase encoding, no EPI distortion correction can be applied in this case.') - cmdline.add_example_usage('DWIs all acquired with a single fixed phase encoding; but additionally a pair of b=0 images with reversed phase encoding to estimate the inhomogeneity field', - 'mrcat b0_ap.mif b0_pa.mif b0_pair.mif -axis 3; dwifslpreproc DWI_in.mif DWI_out.mif -rpe_pair -se_epi b0_pair.mif -pe_dir ap -readout_time 0.72 -align_seepi', - 'Here the two individual b=0 volumes are concatenated into a single 4D image series, and this is provided to the script via the -se_epi option. Note that with the -rpe_pair option used here, which indicates that the SE-EPI image series contains one or more pairs of b=0 images with reversed phase encoding, the FIRST HALF of the volumes in the SE-EPI series must possess the same phase encoding as the input DWI series, while the second half are assumed to contain the opposite phase encoding direction but identical total readout time. Use of the -align_seepi option is advocated as long as its use is valid (more information in the Description section).') - cmdline.add_example_usage('All DWI directions & b-values are acquired twice, with the phase encoding direction of the second acquisition protocol being reversed with respect to the first', - 'mrcat DWI_lr.mif DWI_rl.mif DWI_all.mif -axis 3; dwifslpreproc DWI_all.mif DWI_out.mif -rpe_all -pe_dir lr -readout_time 0.66', - 'Here the two acquisition protocols are concatenated into a single DWI series containing all acquired volumes. The direction indicated via the -pe_dir option should be the direction of phase encoding used in acquisition of the FIRST HALF of volumes in the input DWI series; ie. the first of the two files that was provided to the mrcat command. In this usage scenario, the output DWI series will contain the same number of image volumes as ONE of the acquired DWI series (ie. half of the number in the concatenated series); this is because the script will identify pairs of volumes that possess the same diffusion sensitisation but reversed phase encoding, and perform explicit recombination of those volume pairs in such a way that image contrast in regions of inhomogeneity is determined from the stretched rather than the compressed image.') + cmdline.add_example_usage('DWIs all acquired with a single fixed phase encoding; ' + 'but additionally a pair of b=0 images with reversed phase encoding to estimate the inhomogeneity field', + 'mrcat b0_ap.mif b0_pa.mif b0_pair.mif -axis 3; ' + 'dwifslpreproc DWI_in.mif DWI_out.mif -rpe_pair -se_epi b0_pair.mif -pe_dir ap -readout_time 0.72 -align_seepi', + 'Here the two individual b=0 volumes are concatenated into a single 4D image series, ' + 'and this is provided to the script via the -se_epi option. ' + 'Note that with the -rpe_pair option used here, ' + 'which indicates that the SE-EPI image series contains one or more pairs of b=0 images with reversed phase encoding, ' + 'the FIRST HALF of the volumes in the SE-EPI series must possess the same phase encoding as the input DWI series, ' + 'while the second half are assumed to contain the opposite phase encoding direction but identical total readout time. ' + 'Use of the -align_seepi option is advocated as long as its use is valid ' + '(more information in the Description section).') + cmdline.add_example_usage('All DWI directions & b-values are acquired twice, ' + 'with the phase encoding direction of the second acquisition protocol being reversed with respect to the first', + 'mrcat DWI_lr.mif DWI_rl.mif DWI_all.mif -axis 3; ' + 'dwifslpreproc DWI_all.mif DWI_out.mif -rpe_all -pe_dir lr -readout_time 0.66', + 'Here the two acquisition protocols are concatenated into a single DWI series containing all acquired volumes. ' + 'The direction indicated via the -pe_dir option should be the direction of ' + 'phase encoding used in acquisition of the FIRST HALF of volumes in the input DWI series; ' + 'ie. the first of the two files that was provided to the mrcat command. ' + 'In this usage scenario, ' + 'the output DWI series will contain the same number of image volumes as ONE of the acquired DWI series ' + '(ie. half of the number in the concatenated series); ' + 'this is because the script will identify pairs of volumes that possess the same diffusion sensitisation but reversed phase encoding, ' + 'and perform explicit recombination of those volume pairs in such a way that image contrast in ' + 'regions of inhomogeneity is determined from the stretched rather than the compressed image.') cmdline.add_example_usage('Any acquisition scheme that does not fall into one of the example usages above', - 'mrcat DWI_*.mif DWI_all.mif -axis 3; mrcat b0_*.mif b0_all.mif -axis 3; dwifslpreproc DWI_all.mif DWI_out.mif -rpe_header -se_epi b0_all.mif -align_seepi', - 'With this usage, the relevant phase encoding information is determined entirely based on the contents of the relevant image headers, and dwifslpreproc prepares all metadata for the executed FSL commands accordingly. This can therefore be used if the particular DWI acquisition strategy used does not correspond to one of the simple examples as described in the prior examples. This usage is predicated on the headers of the input files containing appropriately-named key-value fields such that MRtrix3 tools identify them as such. In some cases, conversion from DICOM using MRtrix3 commands will automatically extract and embed this information; however this is not true for all scanner vendors and/or software versions. In the latter case it may be possible to manually provide these metadata; either using the -json_import command-line option of dwifslpreproc, or the -json_import or one of the -import_pe_* command-line options of MRtrix3\'s mrconvert command (and saving in .mif format) prior to running dwifslpreproc.') - cmdline.add_citation('Andersson, J. L. & Sotiropoulos, S. N. An integrated approach to correction for off-resonance effects and subject movement in diffusion MR imaging. NeuroImage, 2015, 125, 1063-1078', is_external=True) - cmdline.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) - cmdline.add_citation('Skare, S. & Bammer, R. Jacobian weighting of distortion corrected EPI data. Proceedings of the International Society for Magnetic Resonance in Medicine, 2010, 5063', condition='If performing recombination of diffusion-weighted volume pairs with opposing phase encoding directions', is_external=True) - cmdline.add_citation('Andersson, J. L.; Skare, S. & Ashburner, J. How to correct susceptibility distortions in spin-echo echo-planar images: application to diffusion tensor imaging. NeuroImage, 2003, 20, 870-888', condition='If performing EPI susceptibility distortion correction', is_external=True) - cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Zsoldos, E. & Sotiropoulos, S. N. Incorporating outlier detection and replacement into a non-parametric framework for movement and distortion correction of diffusion MR images. NeuroImage, 2016, 141, 556-572', condition='If including "--repol" in -eddy_options input', is_external=True) - cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Drobnjak, I.; Zhang, H.; Filippini, N. & Bastiani, M. Towards a comprehensive framework for movement and distortion correction of diffusion MR images: Within volume movement. NeuroImage, 2017, 152, 450-466', condition='If including "--mporder" in -eddy_options input', is_external=True) - cmdline.add_citation('Bastiani, M.; Cottaar, M.; Fitzgibbon, S.P.; Suri, S.; Alfaro-Almagro, F.; Sotiropoulos, S.N.; Jbabdi, S.; Andersson, J.L.R. Automated quality control for within and between studies diffusion MRI data using a non-parametric framework for movement and distortion correction. NeuroImage, 2019, 184, 801-812', condition='If using -eddyqc_text or -eddyqc_all option and eddy_quad is installed', is_external=True) - cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series to be corrected') - cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') - cmdline.add_argument('-json_import', type=app.Parser.FileIn(), metavar='file', help='Import image header information from an associated JSON file (may be necessary to determine phase encoding information)') + 'mrcat DWI_*.mif DWI_all.mif -axis 3; ' + 'mrcat b0_*.mif b0_all.mif -axis 3; ' + 'dwifslpreproc DWI_all.mif DWI_out.mif -rpe_header -se_epi b0_all.mif -align_seepi', + 'With this usage, ' + 'the relevant phase encoding information is determined entirely based on the contents of the relevant image headers, ' + 'and dwifslpreproc prepares all metadata for the executed FSL commands accordingly. ' + 'This can therefore be used if the particular DWI acquisition strategy used does not correspond to one of the simple examples as described in the prior examples. ' + 'This usage is predicated on the headers of the input files containing appropriately-named key-value fields such that MRtrix3 tools identify them as such. ' + 'In some cases, conversion from DICOM using MRtrix3 commands will automatically extract and embed this information; ' + 'however this is not true for all scanner vendors and/or software versions. ' + 'In the latter case it may be possible to manually provide these metadata; ' + 'either using the -json_import command-line option of dwifslpreproc, ' + 'or the -json_import or one of the -import_pe_* command-line options of MRtrix3\'s mrconvert command ' + '(and saving in .mif format) ' + 'prior to running dwifslpreproc.') + cmdline.add_citation('Andersson, J. L. & Sotiropoulos, S. N. ' + 'An integrated approach to correction for off-resonance effects and subject movement in diffusion MR imaging. ' + 'NeuroImage, 2015, 125, 1063-1078', + is_external=True) + cmdline.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. ' + 'Advances in functional and structural MR image analysis and implementation as FSL. ' + 'NeuroImage, 2004, 23, S208-S219', + is_external=True) + cmdline.add_citation('Skare, S. & Bammer, R. ' + 'Jacobian weighting of distortion corrected EPI data. ' + 'Proceedings of the International Society for Magnetic Resonance in Medicine, 2010, 5063', + condition='If performing recombination of diffusion-weighted volume pairs with opposing phase encoding directions', + is_external=True) + cmdline.add_citation('Andersson, J. L.; Skare, S. & Ashburner, J. ' + 'How to correct susceptibility distortions in spin-echo echo-planar images: ' + 'application to diffusion tensor imaging. ' + 'NeuroImage, 2003, 20, 870-888', + condition='If performing EPI susceptibility distortion correction', + is_external=True) + cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Zsoldos, E. & Sotiropoulos, S. N. ' + 'Incorporating outlier detection and replacement into a non-parametric framework for movement and distortion correction of diffusion MR images. ' + 'NeuroImage, 2016, 141, 556-572', + condition='If including "--repol" in -eddy_options input', + is_external=True) + cmdline.add_citation('Andersson, J. L. R.; Graham, M. S.; Drobnjak, I.; Zhang, H.; Filippini, N. & Bastiani, M. ' + 'Towards a comprehensive framework for movement and distortion correction of diffusion MR images: ' + 'Within volume movement. ' + 'NeuroImage, 2017, 152, 450-466', + condition='If including "--mporder" in -eddy_options input', + is_external=True) + cmdline.add_citation('Bastiani, M.; Cottaar, M.; Fitzgibbon, S.P.; Suri, S.; Alfaro-Almagro, F.; Sotiropoulos, S.N.; Jbabdi, S.; Andersson, J.L.R. ' + 'Automated quality control for within and between studies diffusion MRI data using a non-parametric framework for movement and distortion correction. ' + 'NeuroImage, 2019, 184, 801-812', + condition='If using -eddyqc_text or -eddyqc_all option and eddy_quad is installed', + is_external=True) + cmdline.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series to be corrected') + cmdline.add_argument('output', + type=app.Parser.ImageOut(), + help='The output corrected image series') + cmdline.add_argument('-json_import', + type=app.Parser.FileIn(), + metavar='file', + help='Import image header information from an associated JSON file ' + '(may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') - pe_options.add_argument('-pe_dir', metavar='PE', help='Manually specify the phase encoding direction of the input series; can be a signed axis number (e.g. -0, 1, +2), an axis designator (e.g. RL, PA, IS), or NIfTI axis codes (e.g. i-, j, k)') - pe_options.add_argument('-readout_time', metavar='time', type=app.Parser.Float(0.0), help='Manually specify the total readout time of the input series (in seconds)') + pe_options.add_argument('-pe_dir', + metavar='PE', + help='Manually specify the phase encoding direction of the input series; ' + 'can be a signed axis number (e.g. -0, 1, +2), ' + 'an axis designator (e.g. RL, PA, IS), ' + 'or NIfTI axis codes (e.g. i-, j, k)') + pe_options.add_argument('-readout_time', + type=app.Parser.Float(0.0), + metavar='time', + help='Manually specify the total readout time of the input series (in seconds)') distcorr_options = cmdline.add_argument_group('Options for achieving correction of susceptibility distortions') - distcorr_options.add_argument('-se_epi', type=app.Parser.ImageIn(), metavar='image', help='Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series)') - distcorr_options.add_argument('-align_seepi', action='store_true', help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation, and the DWIs (more information in Description section)') - distcorr_options.add_argument('-topup_options', metavar='" TopupOptions"', help='Manually provide additional command-line options to the topup command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to topup)') - distcorr_options.add_argument('-topup_files', metavar='prefix', help='Provide files generated by prior execution of the FSL "topup" command to be utilised by eddy') + distcorr_options.add_argument('-se_epi', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide an additional image series consisting of spin-echo EPI images, ' + 'which is to be used exclusively by topup for estimating the inhomogeneity field ' + '(i.e. it will not form part of the output image series)') + distcorr_options.add_argument('-align_seepi', + action='store_true', + help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation and the DWIs ' + '(more information in Description section)') + distcorr_options.add_argument('-topup_options', + metavar='" TopupOptions"', + help='Manually provide additional command-line options to the topup command ' + '(provide a string within quotation marks that contains at least one space, ' + 'even if only passing a single command-line option to topup)') + distcorr_options.add_argument('-topup_files', + metavar='prefix', + help='Provide files generated by prior execution of the FSL "topup" command to be utilised by eddy') cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'se_epi' ], False ) cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'align_seepi' ], False ) cmdline.flag_mutually_exclusive_options( [ 'topup_files', 'topup_options' ], False ) eddy_options = cmdline.add_argument_group('Options for affecting the operation of the FSL "eddy" command') - eddy_options.add_argument('-eddy_mask', type=app.Parser.ImageIn(), metavar='image', help='Provide a processing mask to use for eddy, instead of having dwifslpreproc generate one internally using dwi2mask') - eddy_options.add_argument('-eddy_slspec', type=app.Parser.FileIn(), metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') - eddy_options.add_argument('-eddy_options', metavar='" EddyOptions"', help='Manually provide additional command-line options to the eddy command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to eddy)') + eddy_options.add_argument('-eddy_mask', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide a processing mask to use for eddy, ' + 'instead of having dwifslpreproc generate one internally using dwi2mask') + eddy_options.add_argument('-eddy_slspec', + type=app.Parser.FileIn(), + metavar='file', + help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') + eddy_options.add_argument('-eddy_options', + metavar='" EddyOptions"', + help='Manually provide additional command-line options to the eddy command ' + '(provide a string within quotation marks that contains at least one space, ' + 'even if only passing a single command-line option to eddy)') eddyqc_options = cmdline.add_argument_group('Options for utilising EddyQC') - eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.DirectoryOut(), metavar='directory', help='Copy the various text-based statistical outputs generated by eddy, and the output of eddy_qc (if installed), into an output directory') - eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.DirectoryOut(), metavar='directory', help='Copy ALL outputs generated by eddy (including images), and the output of eddy_qc (if installed), into an output directory') + eddyqc_options.add_argument('-eddyqc_text', + type=app.Parser.DirectoryOut(), + metavar='directory', + help='Copy the various text-based statistical outputs generated by eddy, ' + 'and the output of eddy_qc (if installed), ' + 'into an output directory') + eddyqc_options.add_argument('-eddyqc_all', + type=app.Parser.DirectoryOut(), + metavar='directory', + help='Copy ALL outputs generated by eddy (including images), ' + 'and the output of eddy_qc (if installed), ' + 'into an output directory') cmdline.flag_mutually_exclusive_options( [ 'eddyqc_text', 'eddyqc_all' ], False ) app.add_dwgrad_export_options(cmdline) app.add_dwgrad_import_options(cmdline) - rpe_options = cmdline.add_argument_group('Options for specifying the acquisition phase-encoding design; note that one of the -rpe_* options MUST be provided') - rpe_options.add_argument('-rpe_none', action='store_true', help='Specify that no reversed phase-encoding image data is being provided; eddy will perform eddy current and motion correction only') - rpe_options.add_argument('-rpe_pair', action='store_true', help='Specify that a set of images (typically b=0 volumes) will be provided for use in inhomogeneity field estimation only (using the -se_epi option)') - rpe_options.add_argument('-rpe_all', action='store_true', help='Specify that ALL DWIs have been acquired with opposing phase-encoding') - rpe_options.add_argument('-rpe_header', action='store_true', help='Specify that the phase-encoding information can be found in the image header(s), and that this is the information that the script should use') + rpe_options = cmdline.add_argument_group('Options for specifying the acquisition phase-encoding design; ' + 'note that one of the -rpe_* options MUST be provided') + rpe_options.add_argument('-rpe_none', + action='store_true', + help='Specify that no reversed phase-encoding image data is being provided; ' + 'eddy will perform eddy current and motion correction only') + rpe_options.add_argument('-rpe_pair', + action='store_true', + help='Specify that a set of images (typically b=0 volumes) will be provided for use in inhomogeneity field estimation only ' + '(using the -se_epi option)') + rpe_options.add_argument('-rpe_all', + action='store_true', + help='Specify that ALL DWIs have been acquired with opposing phase-encoding') + rpe_options.add_argument('-rpe_header', + action='store_true', + help='Specify that the phase-encoding information can be found in the image header(s), ' + 'and that this is the information that the script should use') cmdline.flag_mutually_exclusive_options( [ 'rpe_none', 'rpe_pair', 'rpe_all', 'rpe_header' ], True ) cmdline.flag_mutually_exclusive_options( [ 'rpe_none', 'se_epi' ], False ) # May still technically provide -se_epi even with -rpe_all cmdline.flag_mutually_exclusive_options( [ 'rpe_pair', 'topup_files'] ) # Would involve two separate sources of inhomogeneity field information @@ -93,12 +248,12 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import CONFIG, MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import app, fsl, image, matrix, path, phaseencoding, run, utils #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import app, fsl, image, matrix, phaseencoding, run, utils #pylint: disable=no-name-in-module, import-outside-toplevel if utils.is_windows(): raise MRtrixError('Script cannot run on Windows due to FSL dependency') - image.check_3d_nonunity(path.from_user(app.ARGS.input, False)) + image.check_3d_nonunity(app.ARGS.input) pe_design = '' if app.ARGS.rpe_none: @@ -106,7 +261,8 @@ def execute(): #pylint: disable=unused-variable elif app.ARGS.rpe_pair: pe_design = 'Pair' if not app.ARGS.se_epi: - raise MRtrixError('If using the -rpe_pair option, the -se_epi option must be used to provide the spin-echo EPI data to be used by topup') + raise MRtrixError('If using the -rpe_pair option, ' + 'the -se_epi option must be used to provide the spin-echo EPI data to be used by topup') elif app.ARGS.rpe_all: pe_design = 'All' elif app.ARGS.rpe_header: @@ -124,42 +280,28 @@ def execute(): #pylint: disable=unused-variable if not pe_design == 'None': topup_config_path = os.path.join(fsl_path, 'etc', 'flirtsch', 'b02b0.cnf') if not os.path.isfile(topup_config_path): - raise MRtrixError('Could not find necessary default config file for FSL topup command (expected location: ' + topup_config_path + ')') + raise MRtrixError(f'Could not find necessary default config file for FSL topup command ' + f'(expected location: {topup_config_path})') topup_cmd = fsl.exe_name('topup') if not fsl.eddy_binary(True) and not fsl.eddy_binary(False): raise MRtrixError('Could not find any version of FSL eddy command') fsl_suffix = fsl.suffix() - app.check_output_path(app.ARGS.output) # Export the gradient table to the path requested by the user if necessary grad_export_option = app.read_dwgrad_export_options() - eddyqc_path = None eddyqc_files = [ 'eddy_parameters', 'eddy_movement_rms', 'eddy_restricted_movement_rms', \ 'eddy_post_eddy_shell_alignment_parameters', 'eddy_post_eddy_shell_PE_translation_parameters', \ 'eddy_outlier_report', 'eddy_outlier_map', 'eddy_outlier_n_stdev_map', 'eddy_outlier_n_sqr_stdev_map', \ 'eddy_movement_over_time' ] + eddyqc_path = None if app.ARGS.eddyqc_text: - eddyqc_path = path.from_user(app.ARGS.eddyqc_text, False) + eddyqc_path = app.ARGS.eddyqc_text elif app.ARGS.eddyqc_all: - eddyqc_path = path.from_user(app.ARGS.eddyqc_all, False) + eddyqc_path = app.ARGS.eddyqc_all eddyqc_files.extend([ 'eddy_outlier_free_data.nii.gz', 'eddy_cnr_maps.nii.gz', 'eddy_residuals.nii.gz' ]) - if eddyqc_path: - if os.path.exists(eddyqc_path): - if os.path.isdir(eddyqc_path): - if any(os.path.exists(os.path.join(eddyqc_path, filename)) for filename in eddyqc_files): - if app.FORCE_OVERWRITE: - app.warn('Output eddy QC directory already contains relevant files; these will be overwritten on completion') - else: - raise MRtrixError('Output eddy QC directory already contains relevant files (use -force to override)') - else: - if app.FORCE_OVERWRITE: - app.warn('Target for eddy QC output is not a directory; it will be overwritten on completion') - else: - raise MRtrixError('Target for eddy QC output exists, and is not a directory (use -force to override)') - eddy_manual_options = [] topup_file_userpath = None @@ -168,11 +310,14 @@ def execute(): #pylint: disable=unused-variable eddy_manual_options = app.ARGS.eddy_options.strip().split() # Check for erroneous usages before we perform any data importing if any(entry.startswith('--mask=') for entry in eddy_manual_options): - raise MRtrixError('Cannot provide eddy processing mask via -eddy_options "--mask=..." as manipulations are required; use -eddy_mask option instead') + raise MRtrixError('Cannot provide eddy processing mask via -eddy_options "--mask=..." as manipulations are required; ' + 'use -eddy_mask option instead') if any(entry.startswith('--slspec=') for entry in eddy_manual_options): - raise MRtrixError('Cannot provide eddy slice specification file via -eddy_options "--slspec=..." as manipulations are required; use -eddy_slspec option instead') + raise MRtrixError('Cannot provide eddy slice specification file via -eddy_options "--slspec=..." as manipulations are required; ' + 'use -eddy_slspec option instead') if '--resamp=lsr' in eddy_manual_options: - raise MRtrixError('dwifslpreproc does not currently support least-squares reconstruction; this cannot be simply passed via -eddy_options') + raise MRtrixError('dwifslpreproc does not currently support least-squares reconstruction; ' + 'this cannot be simply passed via -eddy_options') eddy_topup_entry = [entry for entry in eddy_manual_options if entry.startswith('--topup=')] if len(eddy_topup_entry) > 1: raise MRtrixError('Input to -eddy_options contains multiple "--topup=" entries') @@ -181,14 +326,10 @@ def execute(): #pylint: disable=unused-variable # pre-calculated topup output files were provided this way instead if app.ARGS.se_epi: raise MRtrixError('Cannot use both -eddy_options "--topup=" and -se_epi') - topup_file_userpath = path.from_user(eddy_topup_entry[0][len('--topup='):], False) + topup_file_userpath = app.UserPath(eddy_topup_entry[0][len('--topup='):]) eddy_manual_options = [entry for entry in eddy_manual_options if not entry.startswith('--topup=')] - # Don't import slspec file directly; just make sure it exists - if app.ARGS.eddy_slspec and not os.path.isfile(path.from_user(app.ARGS.eddy_slspec, False)): - raise MRtrixError('Unable to find file \"' + app.ARGS.eddy_slspec + '\" provided via -eddy_slspec option') - # Attempt to find pre-generated topup files before constructing the scratch directory topup_input_movpar = None @@ -196,7 +337,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.topup_files: if topup_file_userpath: raise MRtrixError('Cannot use -topup_files option and also specify "... --topup= ..." within content of -eddy_options') - topup_file_userpath = path.from_user(app.ARGS.topup_files, False) + topup_file_userpath = app.ARGS.topup_files execute_applytopup = pe_design != 'None' or topup_file_userpath if execute_applytopup: @@ -209,75 +350,65 @@ def execute(): #pylint: disable=unused-variable # - Path prefix including the underscore # - Path prefix omitting the underscore - def check_movpar(): - if not os.path.isfile(topup_input_movpar): - raise MRtrixError('No topup movement parameter file found based on path "' + topup_file_userpath + '" (expected location: ' + topup_input_movpar + ')') - def find_fieldcoef(fieldcoef_prefix): - fieldcoef_candidates = glob.glob(fieldcoef_prefix + '_fieldcoef.nii*') + fieldcoef_candidates = glob.glob(f'{fieldcoef_prefix}_fieldcoef.nii*') if not fieldcoef_candidates: - raise MRtrixError('No topup field coefficient image found based on path "' + topup_file_userpath + '"') + raise MRtrixError(f'No topup field coefficient image found based on path "{topup_file_userpath}"') if len(fieldcoef_candidates) > 1: - raise MRtrixError('Multiple topup field coefficient images found based on path "' + topup_file_userpath + '": ' + str(fieldcoef_candidates)) + raise MRtrixError(f'Multiple topup field coefficient images found based on path "{topup_file_userpath}": {fieldcoef_candidates}') return fieldcoef_candidates[0] if os.path.isfile(topup_file_userpath): if topup_file_userpath.endswith('_movpar.txt'): - topup_input_movpar = topup_file_userpath - topup_input_fieldcoef = find_fieldcoef(topup_file_userpath[:-len('_movpar.txt')]) - elif topup_file_userpath.endswith('_fieldcoef.nii') or topup_file_userpath.endswith('_fieldcoef.nii.gz'): - topup_input_fieldcoef = topup_file_userpath - topup_input_movpar = topup_file_userpath - if topup_input_movpar.endswith('.gz'): - topup_input_movpar = topup_input_movpar[:-len('.gz')] - topup_input_movpar = topup_input_movpar[:-len('_fieldcoef.nii')] + '_movpar.txt' - check_movpar() + topup_input_movpar = app.Parser.FileIn(topup_file_userpath) + topup_input_fieldcoef = app.Parser.ImageIn(find_fieldcoef(topup_file_userpath[:-len('_movpar.txt')])) + elif any(str(topup_file_userpath).endswith(postfix) for postfix in ('_fieldcoef.nii', '_fieldcoef.nii.gz')): + topup_input_fieldcoef = app.Parser.ImageIn(topup_file_userpath) + topup_input_movpar = topup_file_userpath[:-len('.gz')] if topup_file_userpath.endswith('.gz') else topup_file_userpath + topup_input_movpar = app.Parser.FileIn(topup_input_movpar[:-len('_fieldcoef.nii')] + '_movpar.txt') else: - raise MRtrixError('Unrecognised file "' + topup_file_userpath + '" specified as pre-calculated topup susceptibility field') + raise MRtrixError(f'Unrecognised file "{topup_file_userpath}" specified as pre-calculated topup susceptibility field') else: - topup_input_movpar = topup_file_userpath - if topup_input_movpar[-1] == '_': - topup_input_movpar = topup_input_movpar[:-1] - topup_input_movpar += '_movpar.txt' - check_movpar() - topup_input_fieldcoef = find_fieldcoef(topup_input_movpar[:-len('_movpar.txt')]) + if topup_file_userpath[-1] == '_': + topup_file_userpath = topup_file_userpath[:-1] + topup_input_movpar = app.Parser.FileIn(f'{topup_file_userpath}_movpar.txt') + topup_input_fieldcoef = app.Parser.ImageIn(find_fieldcoef(topup_file_userpath)) # Convert all input images into MRtrix format and store in scratch directory first - app.make_scratch_dir() - + app.activate_scratch_dir() grad_import_option = app.read_dwgrad_import_options() json_import_option = '' if app.ARGS.json_import: - json_import_option = ' -json_import ' + path.from_user(app.ARGS.json_import) - json_export_option = ' -json_export ' + path.to_scratch('dwi.json', True) - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('dwi.mif') + grad_import_option + json_import_option + json_export_option, + json_import_option = f' -json_import {app.ARGS.json_import}' + json_export_option = ' -json_export dwi.json' + run.command(f'mrconvert {app.ARGS.input} dwi.mif {grad_import_option} {json_import_option} {json_export_option}', preserve_pipes=True) if app.ARGS.se_epi: - image.check_3d_nonunity(path.from_user(app.ARGS.se_epi, False)) - run.command('mrconvert ' + path.from_user(app.ARGS.se_epi) + ' ' + path.to_scratch('se_epi.mif'), + image.check_3d_nonunity(app.ARGS.se_epi) + run.command(f'mrconvert {app.ARGS.se_epi} se_epi.mif', preserve_pipes=True) if topup_file_userpath: - run.function(shutil.copyfile, topup_input_movpar, path.to_scratch('field_movpar.txt', False)) + run.function(shutil.copyfile, topup_input_movpar, 'field_movpar.txt') # Can't run field spline coefficients image through mrconvert: # topup encodes voxel sizes within the three NIfTI intent parameters, and # applytopup requires that these be set, but mrconvert will wipe them - run.function(shutil.copyfile, topup_input_fieldcoef, path.to_scratch('field_fieldcoef.nii' + ('.gz' if topup_input_fieldcoef.endswith('.nii.gz') else ''), False)) + run.function(shutil.copyfile, + topup_input_fieldcoef, + 'field_fieldcoef.nii' + ('.gz' if str(topup_input_fieldcoef).endswith('.nii.gz') else '')) if app.ARGS.eddy_mask: - run.command('mrconvert ' + path.from_user(app.ARGS.eddy_mask) + ' ' + path.to_scratch('eddy_mask.mif') + ' -datatype bit', + run.command(['mrconvert', app.ARGS.eddy_mask, 'eddy_mask.mif', '-datatype', 'bit'], preserve_pipes=True) - app.goto_scratch_dir() - # Get information on the input images, and check their validity dwi_header = image.Header('dwi.mif') if not len(dwi_header.size()) == 4: raise MRtrixError('Input DWI must be a 4D image') dwi_num_volumes = dwi_header.size()[3] - app.debug('Number of DWI volumes: ' + str(dwi_num_volumes)) + app.debug(f'Number of DWI volumes: {dwi_num_volumes}') dwi_num_slices = dwi_header.size()[2] - app.debug('Number of DWI slices: ' + str(dwi_num_slices)) + app.debug(f'Number of DWI slices: {dwi_num_slices}') dwi_pe_scheme = phaseencoding.get_scheme(dwi_header) if app.ARGS.se_epi: se_epi_header = image.Header('se_epi.mif') @@ -289,7 +420,9 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('No diffusion gradient table found') grad = dwi_header.keyval()['dw_scheme'] if not len(grad) == dwi_num_volumes: - raise MRtrixError('Number of lines in gradient table (' + str(len(grad)) + ') does not match input image (' + str(dwi_num_volumes) + ' volumes); check your input data') + raise MRtrixError(f'Number of lines in gradient table ({len(grad)}) ' + f'does not match input image ({dwi_num_volumes} volumes); ' + f'check your input data') # Deal with slice timing information for eddy slice-to-volume correction @@ -300,7 +433,8 @@ def execute(): #pylint: disable=unused-variable slice_encoding_direction = dwi_header.keyval()['SliceEncodingDirection'] app.debug('Slice encoding direction: ' + slice_encoding_direction) if not slice_encoding_direction.startswith('k'): - raise MRtrixError('DWI header indicates that 3rd spatial axis is not the slice axis; this is not yet compatible with --mporder option in eddy, nor supported in dwifslpreproc') + raise MRtrixError('DWI header indicates that 3rd spatial axis is not the slice axis; ' + 'this is not yet compatible with --mporder option in eddy, nor supported in dwifslpreproc') slice_encoding_direction = image.axis2dir(slice_encoding_direction) else: app.console('No slice encoding direction information present; assuming third axis corresponds to slices') @@ -312,31 +446,32 @@ def execute(): #pylint: disable=unused-variable # to the scratch directory... if app.ARGS.eddy_slspec: try: - slice_groups = matrix.load_numeric(path.from_user(app.ARGS.eddy_slspec, False), dtype=int) - app.debug('Slice groups: ' + str(slice_groups)) + slice_groups = matrix.load_numeric(app.ARGS.eddy_slspec, dtype=int) + app.debug(f'Slice groups: {slice_groups}') except ValueError: try: - slice_timing = matrix.load_numeric(path.from_user(app.ARGS.eddy_slspec, False), dtype=float) - app.debug('Slice timing: ' + str(slice_timing)) - app.warn('\"slspec\" file provided to FSL eddy is supposed to contain slice indices for slice groups; ' - 'contents of file \"' + app.ARGS.eddy_slspec + '\" appears to instead be slice timings; ' - 'these data have been imported and will be converted to the appropriate format') + slice_timing = matrix.load_numeric(app.ARGS.eddy_slspec, dtype=float) + app.debug('Slice timing: {slice_timing}') + app.warn(f'"slspec" file provided to FSL eddy is supposed to contain slice indices for slice groups; ' + f'contents of file "{app.ARGS.eddy_slspec}" appears to instead be slice timings; ' + f'these data have been imported and will be converted to the appropriate format') if len(slice_timing) != dwi_num_slices: - raise MRtrixError('Cannot use slice timing information from file \"' + app.ARGS.eddy_slspec + '\" for slice-to-volume correction: ' # pylint: disable=raise-missing-from - 'number of entries (' + str(len(slice_timing)) + ') does not match number of slices (' + str(dwi_num_slices) + ')') + raise MRtrixError(f'Cannot use slice timing information from file "{app.ARGS.eddy_slspec}" for slice-to-volume correction: ' # pylint: disable=raise-missing-from + f'number of entries ({len(slice_timing)}) does not match number of slices ({dwi_num_slices})') except ValueError: - raise MRtrixError('Error parsing eddy \"slspec\" file \"' + app.ARGS.eddy_slspec + '\" ' # pylint: disable=raise-missing-from - '(please see FSL eddy help page, specifically the --slspec option)') + raise MRtrixError(f'Error parsing eddy "slspec" file "{app.ARGS.eddy_slspec}" ' # pylint: disable=raise-missing-from + f'(please see FSL eddy help page, specifically the --slspec option)') else: if 'SliceTiming' not in dwi_header.keyval(): raise MRtrixError('Cannot perform slice-to-volume correction in eddy: ' - '-eddy_slspec option not specified, and no slice timing information present in input DWI header') + '-eddy_slspec option not specified, ' + 'and no slice timing information present in input DWI header') slice_timing = dwi_header.keyval()['SliceTiming'] - app.debug('Initial slice timing contents from header: ' + str(slice_timing)) + app.debug(f'Initial slice timing contents from header: {slice_timing}') if slice_timing in ['invalid', 'variable']: - raise MRtrixError('Cannot use slice timing information in image header for slice-to-volume correction: ' - 'data flagged as "' + slice_timing + '"') - # Fudges necessary to maniupulate nature of slice timing data in cases where + raise MRtrixError(f'Cannot use slice timing information in image header for slice-to-volume correction: ' + f'data flagged as "{slice_timing}"') + # Fudges necessary to manipulate nature of slice timing data in cases where # bad JSON formatting has led to the data not being simply a list of floats # (whether from MRtrix3 DICOM conversion or from anything else) if isinstance(slice_timing, str): @@ -362,8 +497,8 @@ def execute(): #pylint: disable=unused-variable 'data are not numeric') from exception app.debug('Re-formatted slice timing contents from header: ' + str(slice_timing)) if len(slice_timing) != dwi_num_slices: - raise MRtrixError('Cannot use slice timing information in image header for slice-to-volume correction: ' - 'number of entries (' + str(len(slice_timing)) + ') does not match number of slices (' + str(dwi_header.size()[2]) + ')') + raise MRtrixError(f'Cannot use slice timing information in image header for slice-to-volume correction: ' + f'number of entries ({len(slice_timing)}) does not match number of slices ({dwi_header.size()[2]})') elif app.ARGS.eddy_slspec: app.warn('-eddy_slspec option provided, but "--mporder=" not provided via -eddy_options; ' 'slice specification file not imported as it would not be utilised by eddy') @@ -380,12 +515,15 @@ def execute(): #pylint: disable=unused-variable if len(shell_bvalues) == len(shell_asymmetries) + 1: shell_bvalues = shell_bvalues[1:] elif len(shell_bvalues) != len(shell_asymmetries): - raise MRtrixError('Number of b-values reported by mrinfo (' + str(len(shell_bvalues)) + ') does not match number of outputs provided by dirstat (' + str(len(shell_asymmetries)) + ')') - for bvalue, asymmetry in zip(shell_bvalues, shell_asymmetries): - if asymmetry >= 0.1: - app.warn('sampling of b=' + str(bvalue) + ' shell is ' + ('strongly' if asymmetry >= 0.4 else 'moderately') + \ - ' asymmetric; distortion correction may benefit from use of: ' + \ - '-eddy_options " ... --slm=linear ... "') + raise MRtrixError(f'Number of b-values reported by mrinfo ({len(shell_bvalues)}) ' + f'does not match number of outputs provided by dirstat ({len(shell_asymmetries)})') + peak_asymmetry = max(shell_asymmetries) + if peak_asymmetry >= 0.1: + severity = 'Strongly' if peak_asymmetry >= 0.4 else 'Moderately' + app.warn(f'{severity} unbalanced diffusion sensitisation gradient scheme detected; ' + 'eddy current distortion correction may benefit from ' + 'presence of second-level model, eg.: ' + '-eddy_options " ... --slm=linear ... "') # Since we want to access user-defined phase encoding information regardless of whether or not @@ -393,11 +531,11 @@ def execute(): #pylint: disable=unused-variable manual_pe_dir = None if app.ARGS.pe_dir: manual_pe_dir = [ float(i) for i in phaseencoding.direction(app.ARGS.pe_dir) ] - app.debug('Manual PE direction: ' + str(manual_pe_dir)) + app.debug(f'Manual PE direction: {manual_pe_dir}') manual_trt = None if app.ARGS.readout_time: manual_trt = float(app.ARGS.readout_time) - app.debug('Manual readout time: ' + str(manual_trt)) + app.debug(f'Manual readout time: {manual_trt}') # Utilise the b-value clustering algorithm in src/dwi/shells.* @@ -456,14 +594,14 @@ def execute(): #pylint: disable=unused-variable line = list(manual_pe_dir) line.append(trt) dwi_manual_pe_scheme = [ line ] * dwi_num_volumes - app.debug('Manual DWI PE scheme for \'None\' PE design: ' + str(dwi_manual_pe_scheme)) + app.debug(f'Manual DWI PE scheme for "None" PE design: {dwi_manual_pe_scheme}') # With 'Pair', also need to construct the manual scheme for SE EPIs elif pe_design == 'Pair': line = list(manual_pe_dir) line.append(trt) dwi_manual_pe_scheme = [ line ] * dwi_num_volumes - app.debug('Manual DWI PE scheme for \'Pair\' PE design: ' + str(dwi_manual_pe_scheme)) + app.debug(f'Manual DWI PE scheme for "Pair" PE design: {dwi_manual_pe_scheme}') if len(se_epi_header.size()) != 4: raise MRtrixError('If using -rpe_pair option, image provided using -se_epi must be a 4D image') se_epi_num_volumes = se_epi_header.size()[3] @@ -475,7 +613,7 @@ def execute(): #pylint: disable=unused-variable line = [ (-i if i else 0.0) for i in manual_pe_dir ] line.append(trt) se_epi_manual_pe_scheme.extend( [ line ] * int(se_epi_num_volumes/2) ) - app.debug('Manual SEEPI PE scheme for \'Pair\' PE design: ' + str(se_epi_manual_pe_scheme)) + app.debug(f'Manual SEEPI PE scheme for "Pair" PE design: {se_epi_manual_pe_scheme}') # If -rpe_all, need to scan through grad and figure out the pairings # This will be required if relying on user-specified phase encode direction @@ -491,7 +629,7 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('If using -rpe_all option, input image must contain an even number of volumes') grads_matched = [ dwi_num_volumes ] * dwi_num_volumes grad_pairs = [ ] - app.debug('Commencing gradient direction matching; ' + str(dwi_num_volumes) + ' volumes') + app.debug(f'Commencing gradient direction matching; {dwi_num_volumes} volumes') for index1 in range(int(dwi_num_volumes/2)): if grads_matched[index1] == dwi_num_volumes: # As yet unpaired for index2 in range(int(dwi_num_volumes/2), dwi_num_volumes): @@ -500,10 +638,10 @@ def execute(): #pylint: disable=unused-variable grads_matched[index1] = index2 grads_matched[index2] = index1 grad_pairs.append([index1, index2]) - app.debug('Matched volume ' + str(index1) + ' with ' + str(index2) + ': ' + str(grad[index1]) + ' ' + str(grad[index2])) + app.debug(f'Matched volume {index1} with {index2}: {grad[index1]} {grad[index2]}') break else: - raise MRtrixError('Unable to determine matching reversed phase-encode direction volume for DWI volume ' + str(index1)) + raise MRtrixError(f'Unable to determine matching reversed phase-encode direction volume for DWI volume {index1}') if not len(grad_pairs) == dwi_num_volumes/2: raise MRtrixError('Unable to determine complete matching DWI volume pairs for reversed phase-encode combination') # Construct manual PE scheme here: @@ -516,7 +654,7 @@ def execute(): #pylint: disable=unused-variable line = [ (-i if i else 0.0) for i in line ] line.append(trt) dwi_manual_pe_scheme.append(line) - app.debug('Manual DWI PE scheme for \'All\' PE design: ' + str(dwi_manual_pe_scheme)) + app.debug(f'Manual DWI PE scheme for "All" PE design: {dwi_manual_pe_scheme}') else: # No manual phase encode direction defined @@ -550,12 +688,14 @@ def execute(): #pylint: disable=unused-variable # relying on earlier code having successfully generated the 'appropriate' # PE scheme for the input volume based on the diffusion gradient table if not scheme_dirs_match(dwi_pe_scheme, dwi_manual_pe_scheme): - app.warn('User-defined phase-encoding direction design does not match what is stored in DWI image header; proceeding with user specification') + app.warn('User-defined phase-encoding direction design does not match what is stored in DWI image header; ' + 'proceeding with user specification') overwrite_dwi_pe_scheme = True if manual_trt: # Compare manual specification to that read from the header if not scheme_times_match(dwi_pe_scheme, dwi_manual_pe_scheme): - app.warn('User-defined total readout time does not match what is stored in DWI image header; proceeding with user specification') + app.warn('User-defined total readout time does not match what is stored in DWI image header; ' + 'proceeding with user specification') overwrite_dwi_pe_scheme = True if overwrite_dwi_pe_scheme: dwi_pe_scheme = dwi_manual_pe_scheme # May be used later for triggering volume recombination @@ -568,7 +708,8 @@ def execute(): #pylint: disable=unused-variable if not manual_pe_dir: raise MRtrixError('No phase encoding information provided either in header or at command-line') if dwi_auto_trt_warning: - app.console('Total readout time not provided at command-line; assuming sane default of ' + str(auto_trt)) + app.console(f'Total readout time not provided at command-line; ' + f'assuming sane default of {auto_trt}') dwi_pe_scheme = dwi_manual_pe_scheme # May be needed later for triggering volume recombination # This may be required by -rpe_all for extracting b=0 volumes while retaining phase-encoding information @@ -587,7 +728,7 @@ def execute(): #pylint: disable=unused-variable if line[3] <= bzero_threshold: break dwi_first_bzero_index += 1 - app.debug('Index of first b=0 image in DWIs is ' + str(dwi_first_bzero_index)) + app.debug(f'Index of first b=0 image in DWIs is {dwi_first_bzero_index}') # Deal with the phase-encoding of the images to be fed to topup (if applicable) @@ -604,7 +745,8 @@ def execute(): #pylint: disable=unused-variable app.console('DWIs and SE-EPI images used for inhomogeneity field estimation are defined on different image grids; ' 'the latter will be automatically re-gridded to match the former') new_se_epi_path = 'se_epi_regrid.mif' - run.command('mrtransform ' + se_epi_path + ' - -reorient_fod no -interp sinc -template dwi.mif | mrcalc - 0.0 -max ' + new_se_epi_path) + run.command(f'mrtransform {se_epi_path} - -reorient_fod no -interp sinc -template dwi.mif | ' + f'mrcalc - 0.0 -max {new_se_epi_path}') app.cleanup(se_epi_path) se_epi_path = new_se_epi_path se_epi_header = image.Header(se_epi_path) @@ -624,11 +766,13 @@ def execute(): #pylint: disable=unused-variable if se_epi_pe_scheme: if manual_pe_dir: if not scheme_dirs_match(se_epi_pe_scheme, se_epi_manual_pe_scheme): - app.warn('User-defined phase-encoding direction design does not match what is stored in SE EPI image header; proceeding with user specification') + app.warn('User-defined phase-encoding direction design does not match what is stored in SE EPI image header; ' + 'proceeding with user specification') overwrite_se_epi_pe_scheme = True if manual_trt: if not scheme_times_match(se_epi_pe_scheme, se_epi_manual_pe_scheme): - app.warn('User-defined total readout time does not match what is stored in SE EPI image header; proceeding with user specification') + app.warn('User-defined total readout time does not match what is stored in SE EPI image header; ' + 'proceeding with user specification') overwrite_se_epi_pe_scheme = True if overwrite_se_epi_pe_scheme: se_epi_pe_scheme = se_epi_manual_pe_scheme @@ -646,7 +790,8 @@ def execute(): #pylint: disable=unused-variable # - Don't have enough information to proceed # - Is this too harsh? (e.g. Have rules by which it may be inferred from the DWI header / command-line) if not se_epi_pe_scheme: - raise MRtrixError('If explicitly including SE EPI images when using -rpe_all option, they must come with their own associated phase-encoding information in the image header') + raise MRtrixError('If explicitly including SE EPI images when using -rpe_all option, ' + 'they must come with their own associated phase-encoding information in the image header') elif pe_design == 'Header': # Criteria: @@ -662,10 +807,12 @@ def execute(): #pylint: disable=unused-variable se_epi_pe_scheme_has_contrast = 'pe_scheme' in se_epi_header.keyval() if not se_epi_pe_scheme_has_contrast: if app.ARGS.align_seepi: - app.console('No phase-encoding contrast present in SE-EPI images; will examine again after combining with DWI b=0 images') + app.console('No phase-encoding contrast present in SE-EPI images; ' + 'will examine again after combining with DWI b=0 images') new_se_epi_path = os.path.splitext(se_epi_path)[0] + '_dwibzeros.mif' # Don't worry about trying to produce a balanced scheme here - run.command('dwiextract dwi.mif - -bzero | mrcat - ' + se_epi_path + ' ' + new_se_epi_path + ' -axis 3') + run.command(f'dwiextract dwi.mif - -bzero | ' + f'mrcat - {se_epi_path} {new_se_epi_path} -axis 3') se_epi_header = image.Header(new_se_epi_path) se_epi_pe_scheme_has_contrast = 'pe_scheme' in se_epi_header.keyval() if se_epi_pe_scheme_has_contrast: @@ -676,10 +823,12 @@ def execute(): #pylint: disable=unused-variable # Delay testing appropriateness of the concatenation of these images # (i.e. differences in contrast) to later else: - raise MRtrixError('No phase-encoding contrast present in SE-EPI images, even after concatenating with b=0 images due to -align_seepi option; ' + raise MRtrixError('No phase-encoding contrast present in SE-EPI images, ' + 'even after concatenating with b=0 images due to -align_seepi option; ' 'cannot perform inhomogeneity field estimation') else: - raise MRtrixError('No phase-encoding contrast present in SE-EPI images; cannot perform inhomogeneity field estimation') + raise MRtrixError('No phase-encoding contrast present in SE-EPI images; ' + 'cannot perform inhomogeneity field estimation') if app.ARGS.align_seepi: @@ -689,9 +838,11 @@ def execute(): #pylint: disable=unused-variable dwi_value = dwi_header.keyval().get(field_name) se_epi_value = se_epi_header.keyval().get(field_name) if dwi_value and se_epi_value and dwi_value != se_epi_value: - app.warn('It appears that the spin-echo EPI images used for inhomogeneity field estimation have a different ' + description + ' to the DWIs being corrected. ' - 'This may cause issues in estimation of the field, as the first DWI b=0 volume will be added to the input series to topup ' - 'due to use of the -align_seepi option.') + app.warn(f'It appears that the spin-echo EPI images used for inhomogeneity field estimation ' + f'have a different {description} to the DWIs being corrected; ' + f'this may cause issues in estimation of the field, ' + f'as the first DWI b=0 volume will be added to the input series to topup ' + f'due to use of the -align_seepi option.') # If we are using the -se_epi option, and hence the input images to topup have not come from the DWIs themselves, # we need to insert the first b=0 DWI volume to the start of the topup input image. Otherwise, the field estimated @@ -705,13 +856,14 @@ def execute(): #pylint: disable=unused-variable if dwi_first_bzero_index == len(grad) and not dwi_bzero_added_to_se_epi: - app.warn('Unable to find b=0 volume in input DWIs to provide alignment between topup and eddy; script will proceed as though the -align_seepi option were not provided') + app.warn('Unable to find b=0 volume in input DWIs to provide alignment between topup and eddy; ' + 'script will proceed as though the -align_seepi option were not provided') # If b=0 volumes from the DWIs have already been added to the SE-EPI image due to an # absence of phase-encoding contrast in the latter, we don't need to perform the following elif not dwi_bzero_added_to_se_epi: - run.command('mrconvert dwi.mif dwi_first_bzero.mif -coord 3 ' + str(dwi_first_bzero_index) + ' -axes 0,1,2') + run.command(f'mrconvert dwi.mif dwi_first_bzero.mif -coord 3 {dwi_first_bzero_index} -axes 0,1,2') dwi_first_bzero_pe = dwi_manual_pe_scheme[dwi_first_bzero_index] if overwrite_dwi_pe_scheme else dwi_pe_scheme[dwi_first_bzero_index] se_epi_pe_sum = [ 0, 0, 0 ] @@ -722,8 +874,11 @@ def execute(): #pylint: disable=unused-variable se_epi_volume_to_remove = index new_se_epi_path = os.path.splitext(se_epi_path)[0] + '_firstdwibzero.mif' if (se_epi_pe_sum == [ 0, 0, 0 ]) and (se_epi_volume_to_remove < len(se_epi_pe_scheme)): - app.console('Balanced phase-encoding scheme detected in SE-EPI series; volume ' + str(se_epi_volume_to_remove) + ' will be removed and replaced with first b=0 from DWIs') - run.command('mrconvert ' + se_epi_path + ' - -coord 3 ' + ','.join([str(index) for index in range(len(se_epi_pe_scheme)) if not index == se_epi_volume_to_remove]) + ' | mrcat dwi_first_bzero.mif - ' + new_se_epi_path + ' -axis 3') + app.console(f'Balanced phase-encoding scheme detected in SE-EPI series; ' + f'volume {se_epi_volume_to_remove} will be removed and replaced with first b=0 from DWIs') + seepi_volumes_to_preserve = ','.join(str(index) for index in range(len(se_epi_pe_scheme)) if not index == se_epi_volume_to_remove) + run.command(f'mrconvert {se_epi_path} - -coord 3 {seepi_volumes_to_preserve} | ' + f'mrcat dwi_first_bzero.mif - {new_se_epi_path} -axis 3') # Also need to update the phase-encoding scheme appropriately if it's being set manually # (if embedded within the image headers, should be updated through the command calls) if se_epi_manual_pe_scheme: @@ -737,10 +892,14 @@ def execute(): #pylint: disable=unused-variable se_epi_manual_pe_scheme = new_se_epi_manual_pe_scheme else: if se_epi_pe_sum == [ 0, 0, 0 ] and se_epi_volume_to_remove == len(se_epi_pe_scheme): - app.console('Phase-encoding scheme of -se_epi image is balanced, but could not find appropriate volume with which to substitute first b=0 volume from DWIs; first b=0 DWI volume will be inserted to start of series, resulting in an unbalanced scheme') + app.console('Phase-encoding scheme of -se_epi image is balanced, ' + 'but could not find appropriate volume with which to substitute first b=0 volume from DWIs; ' + 'first b=0 DWI volume will be inserted to start of series, ' + 'resulting in an unbalanced scheme') else: - app.console('Unbalanced phase-encoding scheme detected in series provided via -se_epi option; first DWI b=0 volume will be inserted to start of series') - run.command('mrcat dwi_first_bzero.mif ' + se_epi_path + ' ' + new_se_epi_path + ' -axis 3') + app.console('Unbalanced phase-encoding scheme detected in series provided via -se_epi option; ' + 'first DWI b=0 volume will be inserted to start of series') + run.command(f'mrcat dwi_first_bzero.mif {se_epi_path} {new_se_epi_path} -axis 3') # Also need to update the phase-encoding scheme appropriately if se_epi_manual_pe_scheme: first_line = list(manual_pe_dir) @@ -765,7 +924,8 @@ def execute(): #pylint: disable=unused-variable # Preferably also make sure that there's some phase-encoding contrast in there... # With -rpe_all, need to write inferred phase-encoding to file and import before using dwiextract so that the phase-encoding # of the extracted b=0's is propagated to the generated b=0 series - run.command('mrconvert dwi.mif' + import_dwi_pe_table_option + ' - | dwiextract - ' + se_epi_path + ' -bzero') + run.command(f'mrconvert dwi.mif {import_dwi_pe_table_option} - | ' + f'dwiextract - {se_epi_path} -bzero') se_epi_header = image.Header(se_epi_path) # If there's no contrast remaining in the phase-encoding scheme, it'll be written to @@ -773,9 +933,12 @@ def execute(): #pylint: disable=unused-variable # In this scenario, we will be unable to run topup, or volume recombination if 'pe_scheme' not in se_epi_header.keyval(): if pe_design == 'All': - raise MRtrixError('DWI header indicates no phase encoding contrast between b=0 images; cannot proceed with volume recombination-based pre-processing') - app.warn('DWI header indicates no phase encoding contrast between b=0 images; proceeding without inhomogeneity field estimation') + raise MRtrixError('DWI header indicates no phase encoding contrast between b=0 images; ' + 'cannot proceed with volume recombination-based pre-processing') + app.warn('DWI header indicates no phase encoding contrast between b=0 images; ' + 'proceeding without inhomogeneity field estimation') execute_topup = False + execute_applytopup = False run.function(os.remove, se_epi_path) se_epi_path = None se_epi_header = None @@ -788,11 +951,12 @@ def execute(): #pylint: disable=unused-variable # then this volume permutation will need to be taken into account if not topup_file_userpath: if dwi_first_bzero_index == len(grad): - app.warn("No image volumes were classified as b=0 by MRtrix3; no permutation of order of DWI volumes can occur " + \ - "(do you need to adjust config file entry BZeroThreshold?)") + app.warn('No image volumes were classified as b=0 by MRtrix3; ' + 'no permutation of order of DWI volumes can occur ' + \ + '(do you need to adjust config file entry BZeroThreshold?)') elif dwi_first_bzero_index: - app.console('First b=0 volume in input DWIs is volume index ' + str(dwi_first_bzero_index) + '; ' - 'this will be permuted to be the first volume (index 0) when eddy is run') + app.console(f'First b=0 volume in input DWIs is volume index {dwi_first_bzero_index}; ' + f'this will be permuted to be the first volume (index 0) when eddy is run') dwi_permvols_preeddy_option = ' -coord 3 ' + \ str(dwi_first_bzero_index) + \ ',0' + \ @@ -805,8 +969,8 @@ def execute(): #pylint: disable=unused-variable (',' + str(dwi_first_bzero_index+1) if dwi_first_bzero_index < dwi_num_volumes-1 else '') + \ (':' + str(dwi_num_volumes-1) if dwi_first_bzero_index < dwi_num_volumes-2 else '') app.debug('mrconvert options for axis permutation:') - app.debug('Pre: ' + str(dwi_permvols_preeddy_option)) - app.debug('Post: ' + str(dwi_permvols_posteddy_option)) + app.debug(f'Pre: {dwi_permvols_preeddy_option}') + app.debug(f'Post: {dwi_permvols_posteddy_option}') @@ -841,19 +1005,22 @@ def execute(): #pylint: disable=unused-variable if int(axis_size%2): odd_axis_count += 1 if odd_axis_count: - app.console(str(odd_axis_count) + ' spatial ' + ('axes of DWIs have' if odd_axis_count > 1 else 'axis of DWIs has') + ' non-even size; ' - 'this will be automatically padded for compatibility with topup, and the extra slice' + ('s' if odd_axis_count > 1 else '') + ' erased afterwards') + app.console(f'{odd_axis_count} spatial {"axes of DWIs have" if odd_axis_count > 1 else "axis of DWIs has"} non-even size; ' + f'this will be automatically padded for compatibility with topup, ' + f'and the extra {"slices" if odd_axis_count > 1 else "slice"} erased afterwards') for axis, axis_size in enumerate(dwi_header.size()[:3]): if int(axis_size%2): - new_se_epi_path = os.path.splitext(se_epi_path)[0] + '_pad' + str(axis) + '.mif' - run.command('mrconvert ' + se_epi_path + ' -coord ' + str(axis) + ' ' + str(axis_size-1) + ' - | mrcat ' + se_epi_path + ' - ' + new_se_epi_path + ' -axis ' + str(axis)) + new_se_epi_path = f'{os.path.splitext(se_epi_path)[0]}_pad{axis}.mif' + run.command(f'mrconvert {se_epi_path} -coord {axis} {axis_size-1} - | ' + f'mrcat {se_epi_path} - {new_se_epi_path} -axis {axis}') app.cleanup(se_epi_path) se_epi_path = new_se_epi_path - new_dwi_path = os.path.splitext(dwi_path)[0] + '_pad' + str(axis) + '.mif' - run.command('mrconvert ' + dwi_path + ' -coord ' + str(axis) + ' ' + str(axis_size-1) + ' -clear dw_scheme - | mrcat ' + dwi_path + ' - ' + new_dwi_path + ' -axis ' + str(axis)) + new_dwi_path = f'{os.path.splitext(dwi_path)[0]}_pad{axis}.mif' + run.command(f'mrconvert {dwi_path} -coord {axis} {axis_size-1} -clear dw_scheme - | ' + f'mrcat {dwi_path} - {new_dwi_path} -axis {axis}') app.cleanup(dwi_path) dwi_path = new_dwi_path - dwi_post_eddy_crop_option += ' -coord ' + str(axis) + ' 0:' + str(axis_size-1) + dwi_post_eddy_crop_option += f' -coord {axis} 0:{axis_size-1}' if axis == slice_encoding_axis: slice_padded = True dwi_num_slices += 1 @@ -877,19 +1044,21 @@ def execute(): #pylint: disable=unused-variable # Do the conversion in preparation for topup - run.command('mrconvert ' + se_epi_path + ' topup_in.nii' + se_epi_manual_pe_table_option + ' -strides -1,+2,+3,+4 -export_pe_table topup_datain.txt') + run.command(f'mrconvert {se_epi_path} topup_in.nii {se_epi_manual_pe_table_option} -strides -1,+2,+3,+4 -export_pe_table topup_datain.txt') app.cleanup(se_epi_path) # Run topup topup_manual_options = '' if app.ARGS.topup_options: topup_manual_options = ' ' + app.ARGS.topup_options.strip() - topup_output = run.command(topup_cmd + ' --imain=topup_in.nii --datain=topup_datain.txt --out=field --fout=field_map' + fsl_suffix + ' --config=' + topup_config_path + ' --verbose' + topup_manual_options) + topup_output = run.command(f'{topup_cmd} --imain=topup_in.nii --datain=topup_datain.txt --out=field ' + f'--fout=field_map{fsl_suffix} --config={topup_config_path} --verbose {topup_manual_options}') + topup_terminal_output = topup_output.stdout + '\n' + topup_output.stderr + '\n' with open('topup_output.txt', 'wb') as topup_output_file: - topup_output_file.write((topup_output.stdout + '\n' + topup_output.stderr + '\n').encode('utf-8', errors='replace')) + topup_output_file.write(topup_terminal_output.encode('utf-8', errors='replace')) if app.VERBOSITY > 1: app.console('Output of topup command:') - sys.stderr.write(topup_output.stdout + '\n' + topup_output.stderr + '\n') + sys.stderr.write(topup_terminal_output) if execute_applytopup: @@ -897,9 +1066,10 @@ def execute(): #pylint: disable=unused-variable # applytopup can't receive the complete DWI input and correct it as a whole, because the phase-encoding # details may vary between volumes if dwi_manual_pe_scheme: - run.command('mrconvert ' + dwi_path + import_dwi_pe_table_option + ' - | mrinfo - -export_pe_eddy applytopup_config.txt applytopup_indices.txt') + run.command(f'mrconvert {dwi_path} {import_dwi_pe_table_option} - | ' + f'mrinfo - -export_pe_eddy applytopup_config.txt applytopup_indices.txt') else: - run.command('mrinfo ' + dwi_path + ' -export_pe_eddy applytopup_config.txt applytopup_indices.txt') + run.command(f'mrinfo {dwi_path} -export_pe_eddy applytopup_config.txt applytopup_indices.txt') # Call applytopup separately for each unique phase-encoding # This should be the most compatible option with more complex phase-encoding acquisition designs, @@ -910,20 +1080,21 @@ def execute(): #pylint: disable=unused-variable applytopup_config = matrix.load_matrix('applytopup_config.txt') applytopup_indices = matrix.load_vector('applytopup_indices.txt', dtype=int) applytopup_volumegroups = [ [ index for index, value in enumerate(applytopup_indices) if value == group ] for group in range(1, len(applytopup_config)+1) ] - app.debug('applytopup_config: ' + str(applytopup_config)) - app.debug('applytopup_indices: ' + str(applytopup_indices)) - app.debug('applytopup_volumegroups: ' + str(applytopup_volumegroups)) + app.debug(f'applytopup_config: {applytopup_config}') + app.debug(f'applytopup_indices: {applytopup_indices}') + app.debug(f'applytopup_volumegroups: {applytopup_volumegroups}') for index, group in enumerate(applytopup_volumegroups): - prefix = os.path.splitext(dwi_path)[0] + '_pe_' + str(index) - input_path = prefix + '.nii' - json_path = prefix + '.json' - temp_path = prefix + '_applytopup.nii' - output_path = prefix + '_applytopup.mif' - run.command('mrconvert ' + dwi_path + ' ' + input_path + ' -coord 3 ' + ','.join(str(value) for value in group) + ' -strides -1,+2,+3,+4 -json_export ' + json_path) - run.command(applytopup_cmd + ' --imain=' + input_path + ' --datain=applytopup_config.txt --inindex=' + str(index+1) + ' --topup=field --out=' + temp_path + ' --method=jac') + prefix = f'{os.path.splitext(dwi_path)[0]}_pe{index}' + input_path = f'{prefix}.nii' + json_path = f'{prefix}.json' + temp_path = f'{prefix}_applytopup.nii' + output_path = f'{prefix}_applytopup.mif' + volumes = ','.join(map(str, group)) + run.command(f'mrconvert {dwi_path} {input_path} -coord 3 {volumes} -strides -1,+2,+3,+4 -json_export {json_path}') + run.command(f'{applytopup_cmd} --imain={input_path} --datain=applytopup_config.txt --inindex={index+1} --topup=field --out={temp_path} --method=jac') app.cleanup(input_path) temp_path = fsl.find_image(temp_path) - run.command('mrconvert ' + temp_path + ' ' + output_path + ' -json_import ' + json_path) + run.command(f'mrconvert {temp_path} {output_path} -json_import {json_path}') app.cleanup(json_path) app.cleanup(temp_path) applytopup_image_list.append(output_path) @@ -937,9 +1108,10 @@ def execute(): #pylint: disable=unused-variable dwi2mask_in_path = applytopup_image_list[0] else: dwi2mask_in_path = 'dwi2mask_in.mif' - run.command('mrcat ' + ' '.join(applytopup_image_list) + ' ' + dwi2mask_in_path + ' -axis 3') - run.command('dwi2mask ' + dwi2mask_algo + ' ' + dwi2mask_in_path + ' ' + dwi2mask_out_path) - run.command('maskfilter ' + dwi2mask_out_path + ' dilate - | mrconvert - eddy_mask.nii -datatype float32 -strides -1,+2,+3') + run.command(['mrcat', applytopup_image_list, dwi2mask_in_path, '-axis', '3']) + run.command(['dwi2mask', dwi2mask_algo, dwi2mask_in_path, dwi2mask_out_path]) + run.command(f'maskfilter {dwi2mask_out_path} dilate - | ' + f'mrconvert - eddy_mask.nii -datatype float32 -strides -1,+2,+3') if len(applytopup_image_list) > 1: app.cleanup(dwi2mask_in_path) app.cleanup(dwi2mask_out_path) @@ -953,8 +1125,9 @@ def execute(): #pylint: disable=unused-variable # Generate a processing mask for eddy based on the uncorrected input DWIs if not app.ARGS.eddy_mask: dwi2mask_out_path = 'dwi2mask_out.mif' - run.command('dwi2mask ' + dwi2mask_algo + ' ' + dwi_path + ' ' + dwi2mask_out_path) - run.command('maskfilter ' + dwi2mask_out_path + ' dilate - | mrconvert - eddy_mask.nii -datatype float32 -strides -1,+2,+3') + run.command(['dwi2mask', dwi2mask_algo, dwi_path, dwi2mask_out_path]) + run.command(f'maskfilter {dwi2mask_out_path} dilate - | ' + f'mrconvert - eddy_mask.nii -datatype float32 -strides -1,+2,+3') app.cleanup(dwi2mask_out_path) @@ -964,9 +1137,9 @@ def execute(): #pylint: disable=unused-variable run.command('mrconvert eddy_mask.mif eddy_mask.nii -datatype float32 -stride -1,+2,+3') else: app.warn('User-provided processing mask for eddy does not match DWI voxel grid; resampling') - run.command('mrtransform eddy_mask.mif - -template ' + dwi_path + ' -interp linear | ' - + 'mrthreshold - -abs 0.5 - | ' - + 'mrconvert - eddy_mask.nii -datatype float32 -stride -1,+2,+3') + run.command(f'mrtransform eddy_mask.mif - -template {dwi_path} -interp linear | ' + f'mrthreshold - -abs 0.5 - | ' + f'mrconvert - eddy_mask.nii -datatype float32 -stride -1,+2,+3') app.cleanup('eddy_mask.mif') # Generate the text file containing slice timing / grouping information if necessary @@ -980,8 +1153,8 @@ def execute(): #pylint: disable=unused-variable if sum(slice_encoding_direction) < 0: slice_timing = reversed(slice_timing) slice_groups = [ [ x[0] for x in g ] for _, g in itertools.groupby(sorted(enumerate(slice_timing), key=lambda x:x[1]), key=lambda x:x[1]) ] #pylint: disable=unused-variable - app.debug('Slice timing: ' + str(slice_timing)) - app.debug('Resulting slice groups: ' + str(slice_groups)) + app.debug(f'Slice timing: {slice_timing}') + app.debug(f'Resulting slice groups: {slice_groups}') # Variable slice_groups may have already been defined in the correct format. # In that instance, there's nothing to do other than write it to file; # UNLESS the slice encoding direction is known to be reversed, in which case @@ -993,8 +1166,8 @@ def execute(): #pylint: disable=unused-variable for group in new_slice_groups: new_slice_groups.append([ dwi_num_slices-index for index in group ]) app.debug('Slice groups reversed due to negative slice encoding direction') - app.debug('Original: ' + str(slice_groups)) - app.debug('New: ' + str(new_slice_groups)) + app.debug(f'Original: {slice_groups}') + app.debug(f'New: {new_slice_groups}') slice_groups = new_slice_groups matrix.save_numeric('slspec.txt', slice_groups, add_to_command_history=False, fmt='%d') @@ -1006,66 +1179,48 @@ def execute(): #pylint: disable=unused-variable # Prepare input data for eddy - run.command('mrconvert ' + dwi_path + import_dwi_pe_table_option + dwi_permvols_preeddy_option + ' eddy_in.nii -strides -1,+2,+3,+4 -export_grad_fsl bvecs bvals -export_pe_eddy eddy_config.txt eddy_indices.txt') + run.command(f'mrconvert {dwi_path} {import_dwi_pe_table_option} {dwi_permvols_preeddy_option} eddy_in.nii ' + f'-strides -1,+2,+3,+4 -export_grad_fsl bvecs bvals -export_pe_eddy eddy_config.txt eddy_indices.txt') app.cleanup(dwi_path) # Run eddy # If a CUDA version is in PATH, run that first; if it fails, re-try using the non-CUDA version - eddy_all_options = '--imain=eddy_in.nii --mask=eddy_mask.nii --acqp=eddy_config.txt --index=eddy_indices.txt --bvecs=bvecs --bvals=bvals' + eddy_in_topup_option + eddy_manual_options + ' --out=dwi_post_eddy --verbose' + eddy_all_options = f'--imain=eddy_in.nii --mask=eddy_mask.nii --acqp=eddy_config.txt --index=eddy_indices.txt ' \ + f'--bvecs=bvecs --bvals=bvals ' \ + f'{eddy_in_topup_option} {eddy_manual_options} --out=dwi_post_eddy --verbose' eddy_cuda_cmd = fsl.eddy_binary(True) eddy_openmp_cmd = fsl.eddy_binary(False) if eddy_cuda_cmd: # If running CUDA version, but OpenMP version is also available, don't stop the script if the CUDA version fails try: - eddy_output = run.command(eddy_cuda_cmd + ' ' + eddy_all_options) + eddy_output = run.command(f'{eddy_cuda_cmd} {eddy_all_options}') except run.MRtrixCmdError as exception_cuda: if not eddy_openmp_cmd: raise with open('eddy_cuda_failure_output.txt', 'wb') as eddy_output_file: eddy_output_file.write(str(exception_cuda).encode('utf-8', errors='replace')) - app.console('CUDA version of \'eddy\' was not successful; attempting OpenMP version') + app.console('CUDA version of "eddy" was not successful; ' + 'attempting OpenMP version') try: eddy_output = run.command(eddy_openmp_cmd + ' ' + eddy_all_options) except run.MRtrixCmdError as exception_openmp: with open('eddy_openmp_failure_output.txt', 'wb') as eddy_output_file: eddy_output_file.write(str(exception_openmp).encode('utf-8', errors='replace')) # Both have failed; want to combine error messages - eddy_cuda_header = ('=' * len(eddy_cuda_cmd)) \ - + '\n' \ - + eddy_cuda_cmd \ - + '\n' \ - + ('=' * len(eddy_cuda_cmd)) \ - + '\n' - eddy_openmp_header = ('=' * len(eddy_openmp_cmd)) \ - + '\n' \ - + eddy_openmp_cmd \ - + '\n' \ - + ('=' * len(eddy_openmp_cmd)) \ - + '\n' - exception_stdout = eddy_cuda_header \ - + exception_cuda.stdout \ - + '\n\n' \ - + eddy_openmp_header \ - + exception_openmp.stdout \ - + '\n\n' - exception_stderr = eddy_cuda_header \ - + exception_cuda.stderr \ - + '\n\n' \ - + eddy_openmp_header \ - + exception_openmp.stderr \ - + '\n\n' - raise run.MRtrixCmdError('eddy* ' + eddy_all_options, - 1, - exception_stdout, - exception_stderr) + eddy_cuda_header = f'{"="*len(eddy_cuda_cmd)}\n{eddy_cuda_cmd}\n{"="*len(eddy_cuda_cmd)}\n' + eddy_openmp_header = f'{"="*len(eddy_openmp_cmd)}\n{eddy_openmp_cmd}\n{"="*len(eddy_openmp_cmd)}\n' + exception_stdout = f'{eddy_cuda_header}{exception_cuda.stdout}\n\n{eddy_openmp_header}{exception_openmp.stdout}\n\n' + exception_stderr = f'{eddy_cuda_header}{exception_cuda.stderr}\n\n{eddy_openmp_header}{exception_openmp.stderr}\n\n' + raise run.MRtrixCmdError(f'eddy* {eddy_all_options}', 1, exception_stdout, exception_stderr) else: - eddy_output = run.command(eddy_openmp_cmd + ' ' + eddy_all_options) + eddy_output = run.command(f'{eddy_openmp_cmd} {eddy_all_options}') + eddy_terminal_output = eddy_output.stdout + '\n' + eddy_output.stderr + '\n' with open('eddy_output.txt', 'wb') as eddy_output_file: - eddy_output_file.write((eddy_output.stdout + '\n' + eddy_output.stderr + '\n').encode('utf-8', errors='replace')) + eddy_output_file.write(eddy_terminal_output.encode('utf-8', errors='replace')) if app.VERBOSITY > 1: app.console('Output of eddy command:') - sys.stderr.write(eddy_output.stdout + '\n' + eddy_output.stderr + '\n') + sys.stderr.write(eddy_terminal_output) app.cleanup('eddy_in.nii') eddy_output_image_path = fsl.find_image('dwi_post_eddy') @@ -1075,7 +1230,9 @@ def execute(): #pylint: disable=unused-variable # if it has, import this into the output image bvecs_path = 'dwi_post_eddy.eddy_rotated_bvecs' if not os.path.isfile(bvecs_path): - app.warn('eddy has not provided rotated bvecs file; using original gradient table. Recommend updating FSL eddy to version 5.0.9 or later.') + app.warn('eddy has not provided rotated bvecs file; ' + 'using original gradient table. ' + 'Recommend updating FSL eddy to version 5.0.9 or later.') bvecs_path = 'bvecs' @@ -1096,32 +1253,32 @@ def execute(): #pylint: disable=unused-variable for eddy_filename in eddyqc_files: if os.path.isfile('dwi_post_eddy.' + eddy_filename): if slice_padded and eddy_filename in [ 'eddy_outlier_map', 'eddy_outlier_n_sqr_stdev_map', 'eddy_outlier_n_stdev_map' ]: - with open('dwi_post_eddy.' + eddy_filename, 'r', encoding='utf-8') as f_eddyfile: + with open(f'dwi_post_eddy.{eddy_filename}', 'r', encoding='utf-8') as f_eddyfile: eddy_data = f_eddyfile.readlines() eddy_data_header = eddy_data[0] eddy_data = eddy_data[1:] for line in eddy_data: line = ' '.join(line.strip().split(' ')[:-1]) - with open('dwi_post_eddy_unpad.' + eddy_filename, 'w', encoding='utf-8') as f_eddyfile: + with open(f'dwi_post_eddy_unpad.{eddy_filename}', 'w', encoding='utf-8') as f_eddyfile: f_eddyfile.write(eddy_data_header + '\n') f_eddyfile.write('\n'.join(eddy_data) + '\n') elif eddy_filename.endswith('.nii.gz'): - run.command('mrconvert dwi_post_eddy.' + eddy_filename + ' dwi_post_eddy_unpad.' + eddy_filename + dwi_post_eddy_crop_option) + run.command(f'mrconvert dwi_post_eddy.{eddy_filename} dwi_post_eddy_unpad.{eddy_filename} {dwi_post_eddy_crop_option}') else: - run.function(os.symlink, 'dwi_post_eddy.' + eddy_filename, 'dwi_post_eddy_unpad.' + eddy_filename) - app.cleanup('dwi_post_eddy.' + eddy_filename) + run.function(os.symlink, f'dwi_post_eddy.{eddy_filename}', f'dwi_post_eddy_unpad.{eddy_filename}') + app.cleanup(f'dwi_post_eddy.{eddy_filename}') progress.increment() if eddy_mporder and slice_padded: - app.debug('Current slice groups: ' + str(slice_groups)) - app.debug('Slice encoding direction: ' + str(slice_encoding_direction)) + app.debug(f'Current slice groups: {slice_groups}') + app.debug(f'Slice encoding direction: {slice_encoding_direction}') # Remove padded slice from slice_groups, write new slspec if sum(slice_encoding_direction) < 0: slice_groups = [ [ index-1 for index in group if index ] for group in slice_groups ] else: slice_groups = [ [ index for index in group if index != dwi_num_slices-1 ] for group in slice_groups ] eddyqc_slspec = 'slspec_unpad.txt' - app.debug('Slice groups after removal: ' + str(slice_groups)) + app.debug(f'Slice groups after removal: {slice_groups}') try: # After this removal, slspec should now be a square matrix assert all(len(group) == len(slice_groups[0]) for group in slice_groups[1:]) @@ -1130,39 +1287,41 @@ def execute(): #pylint: disable=unused-variable matrix.save_numeric(eddyqc_slspec, slice_groups, add_to_command_history=False, fmt='%d') raise - run.command('mrconvert eddy_mask.nii eddy_mask_unpad.nii' + dwi_post_eddy_crop_option) + run.command(f'mrconvert eddy_mask.nii eddy_mask_unpad.nii {dwi_post_eddy_crop_option}') eddyqc_mask = 'eddy_mask_unpad.nii' progress.increment() - run.command('mrconvert ' + fsl.find_image('field_map') + ' field_map_unpad.nii' + dwi_post_eddy_crop_option) + field_map_image = fsl.find_image('field_map') + run.command('mrconvert {field_map_image} field_map_unpad.nii {dwi_post_eddy_crop_option}') eddyqc_fieldmap = 'field_map_unpad.nii' progress.increment() - run.command('mrconvert ' + eddy_output_image_path + ' dwi_post_eddy_unpad.nii.gz' + dwi_post_eddy_crop_option) + run.command(f'mrconvert {eddy_output_image_path} dwi_post_eddy_unpad.nii.gz {dwi_post_eddy_crop_option}') eddyqc_prefix = 'dwi_post_eddy_unpad' progress.done() - eddyqc_options = ' -idx eddy_indices.txt -par eddy_config.txt -b bvals -m ' + eddyqc_mask - if os.path.isfile(eddyqc_prefix + '.eddy_residuals.nii.gz'): - eddyqc_options += ' -g ' + bvecs_path + eddyqc_options = f' -idx eddy_indices.txt -par eddy_config.txt -b bvals -m {eddyqc_mask}' + if os.path.isfile(f'{eddyqc_prefix}.eddy_residuals.nii.gz'): + eddyqc_options += f' -g {bvecs_path}' if execute_topup: - eddyqc_options += ' -f ' + eddyqc_fieldmap + eddyqc_options += f' -f {eddyqc_fieldmap}' if eddy_mporder: - eddyqc_options += ' -s ' + eddyqc_slspec + eddyqc_options += f' -s {eddyqc_slspec}' if app.VERBOSITY > 2: eddyqc_options += ' -v' try: - run.command('eddy_quad ' + eddyqc_prefix + eddyqc_options) + run.command(f'eddy_quad {eddyqc_prefix} {eddyqc_options}') except run.MRtrixCmdError as exception: with open('eddy_quad_failure_output.txt', 'wb') as eddy_quad_output_file: eddy_quad_output_file.write(str(exception).encode('utf-8', errors='replace')) app.debug(str(exception)) - app.warn('Error running automated EddyQC tool \'eddy_quad\'; QC data written to "' + eddyqc_path + '" will be files from "eddy" only') + app.warn(f'Error running automated EddyQC tool "eddy_quad"; ' + f'QC data written to "{eddyqc_path}" will be files from "eddy" only') # Delete the directory if the script only made it partway through try: - shutil.rmtree(eddyqc_prefix + '.qc') + shutil.rmtree(f'{eddyqc_prefix}.qc') except OSError: pass else: - app.console('Command \'eddy_quad\' not found in PATH; skipping') + app.console('Command "eddy_quad" not found in PATH; skipping') # Have to retain these images until after eddyQC is run @@ -1184,7 +1343,7 @@ def execute(): #pylint: disable=unused-variable # The phase-encoding scheme needs to be checked also volume_matchings = [ dwi_num_volumes ] * dwi_num_volumes volume_pairs = [ ] - app.debug('Commencing gradient direction matching; ' + str(dwi_num_volumes) + ' volumes') + app.debug(f'Commencing gradient direction matching; {dwi_num_volumes} volumes') for index1 in range(dwi_num_volumes): if volume_matchings[index1] == dwi_num_volumes: # As yet unpaired for index2 in range(index1+1, dwi_num_volumes): @@ -1194,9 +1353,9 @@ def execute(): #pylint: disable=unused-variable volume_matchings[index1] = index2 volume_matchings[index2] = index1 volume_pairs.append([index1, index2]) - app.debug('Matched volume ' + str(index1) + ' with ' + str(index2) + '\n' + - 'Phase encoding: ' + str(dwi_pe_scheme[index1]) + ' ' + str(dwi_pe_scheme[index2]) + '\n' + - 'Gradients: ' + str(grad[index1]) + ' ' + str(grad[index2])) + app.debug(f'Matched volume {index1} with {index2}\n' + f'Phase encoding: {dwi_pe_scheme[index1]} {dwi_pe_scheme[index2]}\n' + f'Gradients: {grad[index1]} {grad[index2]}') break @@ -1207,11 +1366,13 @@ def execute(): #pylint: disable=unused-variable app.cleanup(fsl.find_image('field_map')) # Convert the resulting volume to the output image, and re-insert the diffusion encoding - run.command('mrconvert ' + eddy_output_image_path + ' result.mif' + dwi_permvols_posteddy_option + dwi_post_eddy_crop_option + stride_option + ' -fslgrad ' + bvecs_path + ' bvals') + run.command(f'mrconvert {eddy_output_image_path} result.mif ' + f'{dwi_permvols_posteddy_option} {dwi_post_eddy_crop_option} {stride_option} -fslgrad {bvecs_path} bvals') app.cleanup(eddy_output_image_path) else: - app.console('Detected matching DWI volumes with opposing phase encoding; performing explicit volume recombination') + app.console('Detected matching DWI volumes with opposing phase encoding; ' + 'performing explicit volume recombination') # Perform a manual combination of the volumes output by eddy, since LSR is disabled @@ -1261,9 +1422,10 @@ def execute(): #pylint: disable=unused-variable field_map_image = fsl.find_image('field_map') field_map_header = image.Header(field_map_image) if not image.match('topup_in.nii', field_map_header, up_to_dim=3): - app.warn('topup output field image has erroneous header; recommend updating FSL to version 5.0.8 or later') + app.warn('topup output field image has erroneous header; ' + 'recommend updating FSL to version 5.0.8 or later') new_field_map_image = 'field_map_fix.mif' - run.command('mrtransform ' + field_map_image + ' -replace topup_in.nii ' + new_field_map_image) + run.command(f'mrtransform {field_map_image} -replace topup_in.nii {new_field_map_image}') app.cleanup(field_map_image) field_map_image = new_field_map_image # In FSL 6.0.0, field map image is erroneously constructed with the same number of volumes as the input image, @@ -1272,7 +1434,7 @@ def execute(): #pylint: disable=unused-variable elif len(field_map_header.size()) == 4: app.console('Correcting erroneous FSL 6.0.0 field map image output') new_field_map_image = 'field_map_fix.mif' - run.command('mrconvert ' + field_map_image + ' -coord 3 0 -axes 0,1,2 ' + new_field_map_image) + run.command(f'mrconvert {field_map_image} -coord 3 0 -axes 0,1,2 {new_field_map_image}') app.cleanup(field_map_image) field_map_image = new_field_map_image app.cleanup('topup_in.nii') @@ -1288,8 +1450,8 @@ def execute(): #pylint: disable=unused-variable # eddy_config.txt and eddy_indices.txt eddy_config = matrix.load_matrix('eddy_config.txt') eddy_indices = matrix.load_vector('eddy_indices.txt', dtype=int) - app.debug('EDDY config: ' + str(eddy_config)) - app.debug('EDDY indices: ' + str(eddy_indices)) + app.debug(f'EDDY config: {eddy_config}') + app.debug(f'EDDY indices: {eddy_indices}') # This section derives, for each phase encoding configuration present, the 'weight' to be applied # to the image during volume recombination, which is based on the Jacobian of the field in the @@ -1297,12 +1459,15 @@ def execute(): #pylint: disable=unused-variable for index, config in enumerate(eddy_config): pe_axis = [ i for i, e in enumerate(config[0:3]) if e != 0][0] sign_multiplier = ' -1.0 -mult' if config[pe_axis] < 0 else '' - field_derivative_path = 'field_deriv_pe_' + str(index+1) + '.mif' - run.command('mrcalc ' + field_map_image + ' ' + str(config[3]) + ' -mult' + sign_multiplier + ' - | mrfilter - gradient - | mrconvert - ' + field_derivative_path + ' -coord 3 ' + str(pe_axis) + ' -axes 0,1,2') - jacobian_path = 'jacobian_' + str(index+1) + '.mif' - run.command('mrcalc 1.0 ' + field_derivative_path + ' -add 0.0 -max ' + jacobian_path) + field_derivative_path = f'field_deriv_pe{index+1}.mif' + total_readout_time = config[3] + run.command(f'mrcalc {field_map_image} {total_readout_time} -mult {sign_multiplier} - | ' + f'mrfilter - gradient - | ' + f'mrconvert - {field_derivative_path} -coord 3 {pe_axis} -axes 0,1,2') + jacobian_path = f'jacobian{index+1}.mif' + run.command(f'mrcalc 1.0 {field_derivative_path} -add 0.0 -max {jacobian_path}') app.cleanup(field_derivative_path) - run.command('mrcalc ' + jacobian_path + ' ' + jacobian_path + ' -mult weight' + str(index+1) + '.mif') + run.command(f'mrcalc {jacobian_path} {jacobian_path} -mult weight{index+1}.mif') app.cleanup(jacobian_path) app.cleanup(field_map_image) @@ -1311,7 +1476,7 @@ def execute(): #pylint: disable=unused-variable # convert it to an uncompressed format before we do anything with it. if eddy_output_image_path.endswith('.gz'): new_eddy_output_image_path = 'dwi_post_eddy_uncompressed.mif' - run.command('mrconvert ' + eddy_output_image_path + ' ' + new_eddy_output_image_path) + run.command(['mrconvert', eddy_output_image_path, new_eddy_output_image_path]) app.cleanup(eddy_output_image_path) eddy_output_image_path = new_eddy_output_image_path @@ -1319,8 +1484,8 @@ def execute(): #pylint: disable=unused-variable # back to their original positions; otherwise, the stored gradient vector directions / phase encode # directions / matched volume pairs are no longer appropriate if dwi_permvols_posteddy_option: - new_eddy_output_image_path = os.path.splitext(eddy_output_image_path)[0] + '_volpermuteundo.mif' - run.command('mrconvert ' + eddy_output_image_path + dwi_permvols_posteddy_option + ' ' + new_eddy_output_image_path) + new_eddy_output_image_path = f'{os.path.splitext(eddy_output_image_path)[0]}_volpermuteundo.mif' + run.command(f'mrconvert {eddy_output_image_path} {new_eddy_output_image_path} {dwi_permvols_posteddy_option}') app.cleanup(eddy_output_image_path) eddy_output_image_path = new_eddy_output_image_path @@ -1330,11 +1495,11 @@ def execute(): #pylint: disable=unused-variable progress = app.ProgressBar('Performing explicit volume recombination', len(volume_pairs)) for index, volumes in enumerate(volume_pairs): pe_indices = [ eddy_indices[i] for i in volumes ] - run.command('mrconvert ' + eddy_output_image_path + ' volume0.mif -coord 3 ' + str(volumes[0])) - run.command('mrconvert ' + eddy_output_image_path + ' volume1.mif -coord 3 ' + str(volumes[1])) + run.command(f'mrconvert {eddy_output_image_path} volume0.mif -coord 3 {volumes[0]}') + run.command(f'mrconvert {eddy_output_image_path} volume1.mif -coord 3 {volumes[1]}') # Volume recombination equation described in Skare and Bammer 2010 - combined_image_path = 'combined' + str(index) + '.mif' - run.command('mrcalc volume0.mif weight' + str(pe_indices[0]) + '.mif -mult volume1.mif weight' + str(pe_indices[1]) + '.mif -mult -add weight' + str(pe_indices[0]) + '.mif weight' + str(pe_indices[1]) + '.mif -add -divide 0.0 -max ' + combined_image_path) + combined_image_path = f'combined{index}.mif' + run.command(f'mrcalc volume0.mif weight{pe_indices[0]}.mif -mult volume1.mif weight{pe_indices[1]}.mif -mult -add weight{pe_indices[0]}.mif weight{pe_indices[1]}.mif -add -divide 0.0 -max {combined_image_path}') combined_image_list.append(combined_image_path) run.function(os.remove, 'volume0.mif') run.function(os.remove, 'volume1.mif') @@ -1343,7 +1508,7 @@ def execute(): #pylint: disable=unused-variable app.cleanup(eddy_output_image_path) for index in range(0, len(eddy_config)): - app.cleanup('weight' + str(index+1) + '.mif') + app.cleanup(f'weight{index+1}.mif') # Finally the recombined volumes must be concatenated to produce the resulting image series combine_command = ['mrcat', combined_image_list, '-', '-axis', '3', '|', \ @@ -1365,18 +1530,21 @@ def execute(): #pylint: disable=unused-variable if os.path.exists(eddyqc_prefix + '.' + filename): # If this is an image, and axis padding was applied, want to undo the padding if filename.endswith('.nii.gz') and dwi_post_eddy_crop_option: - run.command('mrconvert ' + eddyqc_prefix + '.' + filename + ' ' + shlex.quote(os.path.join(eddyqc_path, filename)) + dwi_post_eddy_crop_option, force=app.FORCE_OVERWRITE) + run.command(f'mrconvert {eddyqc_prefix}.{filename} {shlex.quote(os.path.join(eddyqc_path, filename))} {dwi_post_eddy_crop_option}', + force=app.FORCE_OVERWRITE) else: - run.function(shutil.copy, eddyqc_prefix + '.' + filename, os.path.join(eddyqc_path, filename)) + run.function(shutil.copy, f'{eddyqc_prefix}.{filename}', os.path.join(eddyqc_path, filename)) # Also grab any files generated by the eddy qc tool QUAD if os.path.isdir(eddyqc_prefix + '.qc'): if app.FORCE_OVERWRITE and os.path.exists(os.path.join(eddyqc_path, 'quad')): run.function(shutil.rmtree, os.path.join(eddyqc_path, 'quad')) - run.function(shutil.copytree, eddyqc_prefix + '.qc', os.path.join(eddyqc_path, 'quad')) + run.function(shutil.copytree, f'{eddyqc_prefix}.qc', os.path.join(eddyqc_path, 'quad')) # Also grab the brain mask that was provided to eddy if -eddyqc_all was specified if app.ARGS.eddyqc_all: if dwi_post_eddy_crop_option: - run.command('mrconvert eddy_mask.nii ' + shlex.quote(os.path.join(eddyqc_path, 'eddy_mask.nii')) + dwi_post_eddy_crop_option, force=app.FORCE_OVERWRITE) + mask_export_path = shlex.quote(os.path.join(eddyqc_path, 'eddy_mask.nii')) + run.command(f'mrconvert eddy_mask.nii {mask_export_path} {dwi_post_eddy_crop_option}', + force=app.FORCE_OVERWRITE) else: run.function(shutil.copy, 'eddy_mask.nii', os.path.join(eddyqc_path, 'eddy_mask.nii')) app.cleanup('eddy_mask.nii') @@ -1407,7 +1575,9 @@ def execute(): #pylint: disable=unused-variable # Finish! - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output) + grad_export_option, mrconvert_keyval='output.json', force=app.FORCE_OVERWRITE) + run.command(f'mrconvert result.mif {app.ARGS.output} {grad_export_option}', + mrconvert_keyval='output.json', + force=app.FORCE_OVERWRITE) diff --git a/bin/dwigradcheck b/bin/dwigradcheck index f11ad6c1cd..9b32ba819a 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -27,11 +27,21 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_description('Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to ' 'derive a binary mask image to be used for streamline seeding and to constrain streamline propagation. ' 'More information on mask derivation from DWI data can be found at the following link: \n' - 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/dwi_preprocessing/masking.html') - cmdline.add_citation('Jeurissen, B.; Leemans, A.; Sijbers, J. Automated correction of improperly rotated diffusion gradient orientations in diffusion weighted MRI. Medical Image Analysis, 2014, 18(7), 953-962') - cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series to be checked') - cmdline.add_argument('-mask', metavar='image', type=app.Parser.ImageIn(), help='Provide a mask image within which to seed & constrain tracking') - cmdline.add_argument('-number', type=app.Parser.Int(1), default=10000, help='Set the number of tracks to generate for each test') + f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') + cmdline.add_citation('Jeurissen, B.; Leemans, A.; Sijbers, J. ' + 'Automated correction of improperly rotated diffusion gradient orientations in diffusion weighted MRI. ' + 'Medical Image Analysis, 2014, 18(7), 953-962') + cmdline.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series to be checked') + cmdline.add_argument('-mask', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide a mask image within which to seed & constrain tracking') + cmdline.add_argument('-number', + type=app.Parser.Int(1), + default=10000, + help='Set the number of tracks to generate for each test') app.add_dwgrad_export_options(cmdline) app.add_dwgrad_import_options(cmdline) @@ -41,32 +51,28 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import CONFIG, MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import app, image, matrix, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import app, image, matrix, run #pylint: disable=no-name-in-module, import-outside-toplevel - image_dimensions = image.Header(path.from_user(app.ARGS.input, False)).size() + image_dimensions = image.Header(app.ARGS.input).size() if len(image_dimensions) != 4: raise MRtrixError('Input image must be a 4D image') if min(image_dimensions) == 1: raise MRtrixError('Cannot perform tractography on an image with a unity dimension') num_volumes = image_dimensions[3] - app.make_scratch_dir() - + app.activate_scratch_dir() # Make sure the image data can be memory-mapped - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('data.mif') + ' -strides 0,0,0,1 -datatype float32', + run.command(f'mrconvert {app.ARGS.input} data.mif -strides 0,0,0,1 -datatype float32', preserve_pipes=True) - if app.ARGS.grad: - shutil.copy(path.from_user(app.ARGS.grad, False), path.to_scratch('grad.b', False)) + shutil.copy(app.ARGS.grad, 'grad.b') elif app.ARGS.fslgrad: - shutil.copy(path.from_user(app.ARGS.fslgrad[0], False), path.to_scratch('bvecs', False)) - shutil.copy(path.from_user(app.ARGS.fslgrad[1], False), path.to_scratch('bvals', False)) + shutil.copy(app.ARGS.fslgrad[0], 'bvecs') + shutil.copy(app.ARGS.fslgrad[1], 'bvals') if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit', + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], preserve_pipes=True) - app.goto_scratch_dir() - # Make sure we have gradient table stored externally to header in both MRtrix and FSL formats if not os.path.isfile('grad.b'): if os.path.isfile('bvecs'): @@ -94,10 +100,10 @@ def execute(): #pylint: disable=unused-variable # Note that gradient table must be explicitly loaded, since there may not # be one in the image header (user may be relying on -grad or -fslgrad input options) if not os.path.exists('mask.mif'): - run.command('dwi2mask ' + CONFIG['Dwi2maskAlgorithm'] + ' data.mif mask.mif -grad grad.b') + run.command(['dwi2mask', CONFIG['Dwi2maskAlgorithm'], 'data.mif', 'mask.mif', '-grad', 'grad.b']) # How many tracks are we going to generate? - number_option = ' -select ' + str(app.ARGS.number) + number_option = f' -select {app.ARGS.number}' # What variations of gradient errors can we conceive? @@ -122,13 +128,14 @@ def execute(): #pylint: disable=unused-variable # List where the first element is the mean length lengths = [ ] - progress = app.ProgressBar('Testing gradient table alterations (0 of ' + str(total_tests) + ')', total_tests) + progress = app.ProgressBar(f'Testing gradient table alterations (0 of {total_tests})', total_tests) for flip in axis_flips: for permutation in axis_permutations: for basis in grad_basis: - suffix = '_flip' + str(flip) + '_perm' + ''.join(str(item) for item in permutation) + '_' + basis + perm = ''.join(map(str, permutation)) + suffix = f'_flip{flip}_perm{perm}_{basis}' if basis == 'scanner': @@ -143,12 +150,12 @@ def execute(): #pylint: disable=unused-variable grad = [ [ row[permutation[0]], row[permutation[1]], row[permutation[2]], row[3] ] for row in grad ] # Create the gradient table file - grad_path = 'grad' + suffix + '.b' + grad_path = f'grad{suffix}.b' with open(grad_path, 'w', encoding='utf-8') as grad_file: for line in grad: grad_file.write (','.join([str(v) for v in line]) + '\n') - grad_option = ' -grad ' + grad_path + grad_option = f' -grad {grad_path}' elif basis == 'image': @@ -164,19 +171,20 @@ def execute(): #pylint: disable=unused-variable for line in grad: bvecs_file.write (' '.join([str(v) for v in line]) + '\n') - grad_option = ' -fslgrad ' + grad_path + ' bvals' + grad_option = f' -fslgrad {grad_path} bvals' # Run the tracking experiment - run.command('tckgen -algorithm tensor_det data.mif' + grad_option + ' -seed_image mask.mif -mask mask.mif' + number_option + ' -minlength 0 -downsample 5 tracks' + suffix + '.tck') + run.command(f'tckgen -algorithm tensor_det data.mif -seed_image mask.mif -mask mask.mif -minlength 0 -downsample 5 tracks{suffix}.tck ' + f'{grad_option} {number_option}') # Get the mean track length - meanlength=float(run.command('tckstats tracks' + suffix + '.tck -output mean -ignorezero').stdout) + meanlength=float(run.command(f'tckstats tracks{suffix}.tck -output mean -ignorezero').stdout) # Add to the database lengths.append([meanlength,flip,permutation,basis]) # Increament the progress bar - progress.increment('Testing gradient table alterations (' + str(len(lengths)) + ' of ' + str(total_tests) + ')') + progress.increment(f'Testing gradient table alterations ({len(lengths)} of {total_tests})') progress.done() @@ -189,10 +197,11 @@ def execute(): #pylint: disable=unused-variable sys.stderr.write('Mean length Axis flipped Axis permutations Axis basis\n') for line in lengths: if isinstance(line[1], numbers.Number): - flip_str = "{:4d}".format(line[1]) + flip_str = f'{line[1]:4d}' else: flip_str = line[1] - sys.stderr.write("{:5.2f}".format(line[0]) + ' ' + flip_str + ' ' + str(line[2]) + ' ' + line[3] + '\n') + length_string = '{line[0]:5.2f}' + sys.stderr.write(f'{length_string} {flip_str} {line[2]} {line[3]}\n') # If requested, extract what has been detected as the best gradient table, and @@ -200,12 +209,14 @@ def execute(): #pylint: disable=unused-variable grad_export_option = app.read_dwgrad_export_options() if grad_export_option: best = lengths[0] - suffix = '_flip' + str(best[1]) + '_perm' + ''.join(str(item) for item in best[2]) + '_' + best[3] + perm = ''.join(map(str, best[2])) + suffix = f'_flip{best[1]}_perm{perm}_{best[3]}' if best[3] == 'scanner': - grad_import_option = ' -grad grad' + suffix + '.b' + grad_import_option = f' -grad grad{suffix}.b' elif best[3] == 'image': - grad_import_option = ' -fslgrad bvecs' + suffix + ' bvals' - run.command('mrinfo data.mif' + grad_import_option + grad_export_option, force=app.FORCE_OVERWRITE) + grad_import_option = f' -fslgrad bvecs{suffix} bvals' + run.command(f'mrinfo data.mif {grad_import_option} {grad_export_option}', + force=app.FORCE_OVERWRITE) # Execute the script diff --git a/bin/dwinormalise b/bin/dwinormalise index e838a18508..6f5ee6d49c 100755 --- a/bin/dwinormalise +++ b/bin/dwinormalise @@ -21,8 +21,10 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Perform various forms of intensity normalisation of DWIs') cmdline.add_description('This script provides access to different techniques for globally scaling the intensity of diffusion-weighted images. ' - 'The different algorithms have different purposes, and different requirements with respect to the data with which they must be provided & will produce as output. ' - 'Further information on the individual algorithms available can be accessed via their individual help pages; eg. "dwinormalise group -help".') + 'The different algorithms have different purposes, ' + 'and different requirements with respect to the data with which they must be provided & will produce as output. ' + 'Further information on the individual algorithms available can be accessed via their individual help pages; ' + 'eg. "dwinormalise group -help".') # Import the command-line settings for all algorithms found in the relevant directory algorithm.usage(cmdline) @@ -34,7 +36,6 @@ def execute(): #pylint: disable=unused-variable # Find out which algorithm the user has requested alg = algorithm.get_module(app.ARGS.algorithm) - alg.check_output_paths() # From here, the script splits depending on what algorithm is being used alg.execute() diff --git a/bin/dwishellmath b/bin/dwishellmath index 718ed00cec..76cfcf7e2d 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -23,12 +23,22 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Daan Christiaens (daan.christiaens@kcl.ac.uk)') cmdline.set_synopsis('Apply an mrmath operation to each b-value shell in a DWI series') - cmdline.add_description('The output of this command is a 4D image, where ' - 'each volume corresponds to a b-value shell (in order of increasing b-value), and ' - 'the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell.') - cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input diffusion MRI series') - cmdline.add_argument('operation', choices=SUPPORTED_OPS, help='The operation to be applied to each shell; this must be one of the following: ' + ', '.join(SUPPORTED_OPS)) - cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output image series') + cmdline.add_description('The output of this command is a 4D image, ' + 'where each volume corresponds to a b-value shell ' + '(in order of increasing b-value), ' + 'an the intensities within each volume correspond to the chosen statistic having been ' + 'computed from across the DWI volumes belonging to that b-value shell.') + cmdline.add_argument('input', + type=app.Parser.ImageIn(), + help='The input diffusion MRI series') + cmdline.add_argument('operation', + choices=SUPPORTED_OPS, + help='The operation to be applied to each shell; ' + 'this must be one of the following: ' + + ', '.join(SUPPORTED_OPS)) + cmdline.add_argument('output', + type=app.Parser.ImageOut(), + help='The output image series') cmdline.add_example_usage('To compute the mean diffusion-weighted signal in each b-value shell', 'dwishellmath dwi.mif mean shellmeans.mif') app.add_dwgrad_import_options(cmdline) @@ -36,39 +46,39 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable from mrtrix3 import MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel - from mrtrix3 import app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel # check inputs and outputs - dwi_header = image.Header(path.from_user(app.ARGS.input, False)) + dwi_header = image.Header(app.ARGS.input) if len(dwi_header.size()) != 4: raise MRtrixError('Input image must be a 4D image') gradimport = app.read_dwgrad_import_options() if not gradimport and 'dw_scheme' not in dwi_header.keyval(): raise MRtrixError('No diffusion gradient table provided, and none present in image header') - app.check_output_path(app.ARGS.output) # import data and gradient table - app.make_scratch_dir() - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif') + gradimport + ' -strides 0,0,0,1', + app.activate_scratch_dir() + run.command(f'mrconvert {app.ARGS.input} in.mif {gradimport} -strides 0,0,0,1', preserve_pipes=True) - app.goto_scratch_dir() # run per-shell operations files = [] for index, bvalue in enumerate(image.mrinfo('in.mif', 'shell_bvalues').split()): - filename = 'shell-{:02d}.mif'.format(index) - run.command('dwiextract -shells ' + bvalue + ' in.mif - | mrmath -axis 3 - ' + app.ARGS.operation + ' ' + filename) + filename = f'shell-{index:02d}.mif' + run.command(f'dwiextract -shells {bvalue} in.mif - | ' + f'mrmath -axis 3 - {app.ARGS.operation} {filename}') files.append(filename) if len(files) > 1: # concatenate to output file - run.command('mrcat -axis 3 ' + ' '.join(files) + ' out.mif') - run.command('mrconvert out.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrcat', '-axis', '3', files, 'out.mif']) + run.command(['mrconvert', 'out.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) else: # make a 4D image with one volume - app.warn('Only one unique b-value present in DWI data; command mrmath with -axis 3 option may be preferable') - run.command('mrconvert ' + files[0] + ' ' + path.from_user(app.ARGS.output) + ' -axes 0,1,2,-1', - mrconvert_keyval=path.from_user(app.ARGS.input, False), + app.warn('Only one unique b-value present in DWI data; ' + 'command mrmath with -axis 3 option may be preferable') + run.command(['mrconvert', files[0], app.ARGS.output, '-axes', '0,1,2,-1'], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/bin/for_each b/bin/for_each index d0b4c8d893..1743bfd760 100755 --- a/bin/for_each +++ b/bin/for_each @@ -31,33 +31,104 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import _version #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') cmdline.set_synopsis('Perform some arbitrary processing step for each of a set of inputs') - cmdline.add_description('This script greatly simplifies various forms of batch processing by enabling the execution of a command (or set of commands) independently for each of a set of inputs.') + cmdline.add_description('This script greatly simplifies various forms of batch processing by enabling the execution of a command ' + '(or set of commands) ' + 'independently for each of a set of inputs.') cmdline.add_description('More information on use of the for_each command can be found at the following link: \n' - 'https://mrtrix.readthedocs.io/en/' + _version.__tag__ + '/tips_and_tricks/batch_processing_with_foreach.html') - cmdline.add_description('The way that this batch processing capability is achieved is by providing basic text substitutions, which simplify the formation of valid command strings based on the unique components of the input strings on which the script is instructed to execute. This does however mean that the items to be passed as inputs to the for_each command (e.g. file / directory names) MUST NOT contain any instances of these substitution strings, as otherwise those paths will be corrupted during the course of the substitution.') - cmdline.add_description('The available substitutions are listed below (note that the -test command-line option can be used to ensure correct command string formation prior to actually executing the commands):') - cmdline.add_description(' - IN: The full matching pattern, including leading folders. For example, if the target list contains a file "folder/image.mif", any occurrence of "IN" will be substituted with "folder/image.mif".') - cmdline.add_description(' - NAME: The basename of the matching pattern. For example, if the target list contains a file "folder/image.mif", any occurrence of "NAME" will be substituted with "image.mif".') - cmdline.add_description(' - PRE: The prefix of the input pattern (the basename stripped of its extension). For example, if the target list contains a file "folder/my.image.mif.gz", any occurrence of "PRE" will be substituted with "my.image".') - cmdline.add_description(' - UNI: The unique part of the input after removing any common prefix and common suffix. For example, if the target list contains files: "folder/001dwi.mif", "folder/002dwi.mif", "folder/003dwi.mif", any occurrence of "UNI" will be substituted with "001", "002", "003".') - cmdline.add_description('Note that due to a limitation of the Python "argparse" module, any command-line OPTIONS that the user intends to provide specifically to the for_each script must appear BEFORE providing the list of inputs on which for_each is intended to operate. While command-line options provided as such will be interpreted specifically by the for_each script, any command-line options that are provided AFTER the COLON separator will form part of the executed COMMAND, and will therefore be interpreted as command-line options having been provided to that underlying command.') + f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/tips_and_tricks/batch_processing_with_foreach.html') + cmdline.add_description('The way that this batch processing capability is achieved is by providing basic text substitutions, ' + 'which simplify the formation of valid command strings based on the unique components of the input strings on which the script is instructed to execute. ' + 'This does however mean that the items to be passed as inputs to the for_each command ' + '(e.g. file / directory names) ' + 'MUST NOT contain any instances of these substitution strings, ' + 'as otherwise those paths will be corrupted during the course of the substitution.') + cmdline.add_description('The available substitutions are listed below ' + '(note that the -test command-line option can be used to ensure correct command string formation prior to actually executing the commands):') + cmdline.add_description(' - IN: ' + 'The full matching pattern, including leading folders. ' + 'For example, if the target list contains a file "folder/image.mif", ' + 'any occurrence of "IN" will be substituted with "folder/image.mif".') + cmdline.add_description(' - NAME: ' + 'The basename of the matching pattern. ' + 'For example, if the target list contains a file "folder/image.mif", ' + 'any occurrence of "NAME" will be substituted with "image.mif".') + cmdline.add_description(' - PRE: ' + 'The prefix of the input pattern ' + '(the basename stripped of its extension). ' + 'For example, if the target list contains a file "folder/my.image.mif.gz", ' + 'any occurrence of "PRE" will be substituted with "my.image".') + cmdline.add_description(' - UNI: ' + 'The unique part of the input after removing any common prefix and common suffix. ' + 'For example, if the target list contains files: "folder/001dwi.mif", "folder/002dwi.mif", "folder/003dwi.mif", ' + 'any occurrence of "UNI" will be substituted with "001", "002", "003".') + cmdline.add_description('Note that due to a limitation of the Python "argparse" module, ' + 'any command-line OPTIONS that the user intends to provide specifically to the for_each script ' + 'must appear BEFORE providing the list of inputs on which for_each is intended to operate. ' + 'While command-line options provided as such will be interpreted specifically by the for_each script, ' + 'any command-line options that are provided AFTER the COLON separator will form part of the executed COMMAND, ' + 'and will therefore be interpreted as command-line options having been provided to that underlying command.') cmdline.add_example_usage('Demonstration of basic usage syntax', 'for_each folder/*.mif : mrinfo IN', - 'This will run the "mrinfo" command for every .mif file present in "folder/". Note that the compulsory colon symbol is used to separate the list of items on which for_each is being instructed to operate, from the command that is intended to be run for each input.') + 'This will run the "mrinfo" command for every .mif file present in "folder/". ' + 'Note that the compulsory colon symbol is used to separate the list of items on which for_each is being instructed to operate, ' + 'from the command that is intended to be run for each input.') cmdline.add_example_usage('Multi-threaded use of for_each', 'for_each -nthreads 4 freesurfer/subjects/* : recon-all -subjid NAME -all', - 'In this example, for_each is instructed to run the FreeSurfer command \'recon-all\' for all subjects within the \'subjects\' directory, with four subjects being processed in parallel at any one time. Whenever processing of one subject is completed, processing for a new unprocessed subject will commence. This technique is useful for improving the efficiency of running single-threaded commands on multi-core systems, as long as the system possesses enough memory to support such parallel processing. Note that in the case of multi-threaded commands (which includes many MRtrix3 commands), it is generally preferable to permit multi-threaded execution of the command on a single input at a time, rather than processing multiple inputs in parallel.') + 'In this example, ' + 'for_each is instructed to run the FreeSurfer command "recon-all" for all subjects within the "subjects" directory, ' + 'with four subjects being processed in parallel at any one time. ' + 'Whenever processing of one subject is completed, ' + 'processing for a new unprocessed subject will commence. ' + 'This technique is useful for improving the efficiency of running single-threaded commands on multi-core systems, ' + 'as long as the system possesses enough memory to support such parallel processing. ' + 'Note that in the case of multi-threaded commands ' + '(which includes many MRtrix3 commands), ' + 'it is generally preferable to permit multi-threaded execution of the command on a single input at a time, ' + 'rather than processing multiple inputs in parallel.') cmdline.add_example_usage('Excluding specific inputs from execution', 'for_each *.nii -exclude 001.nii : mrconvert IN PRE.mif', - 'Particularly when a wildcard is used to define the list of inputs for for_each, it is possible in some instances that this list will include one or more strings for which execution should in fact not be performed; for instance, if a command has already been executed for one or more files, and then for_each is being used to execute the same command for all other files. In this case, the -exclude option can be used to effectively remove an item from the list of inputs that would otherwise be included due to the use of a wildcard (and can be used more than once to exclude more than one string). In this particular example, mrconvert is instructed to perform conversions from NIfTI to MRtrix image formats, for all except the first image in the directory. Note that any usages of this option must appear AFTER the list of inputs. Note also that the argument following the -exclude option can alternatively be a regular expression, in which case any inputs for which a match to the expression is found will be excluded from processing.') + 'Particularly when a wildcard is used to define the list of inputs for for_each, ' + 'it is possible in some instances that this list will include one or more strings for which execution should in fact not be performed; ' + 'for instance, if a command has already been executed for one or more files, ' + 'and then for_each is being used to execute the same command for all other files. ' + 'In this case, ' + 'the -exclude option can be used to effectively remove an item from the list of inputs that would otherwise be included due to the use of a wildcard ' + '(and can be used more than once to exclude more than one string). ' + 'In this particular example, ' + 'mrconvert is instructed to perform conversions from NIfTI to MRtrix image formats, ' + 'for all except the first image in the directory. ' + 'Note that any usages of this option must appear AFTER the list of inputs. ' + 'Note also that the argument following the -exclude option can alternatively be a regular expression, ' + 'in which case any inputs for which a match to the expression is found will be excluded from processing.') cmdline.add_example_usage('Testing the command string substitution', 'for_each -test * : mrconvert IN PRE.mif', - 'By specifying the -test option, the script will print to the terminal the results of text substitutions for all of the specified inputs, but will not actually execute those commands. It can therefore be used to verify that the script is receiving the intended set of inputs, and that the text substitutions on those inputs lead to the intended command strings.') - cmdline.add_argument('inputs', help='Each of the inputs for which processing should be run', nargs='+') - cmdline.add_argument('colon', help='Colon symbol (":") delimiting the for_each inputs & command-line options from the actual command to be executed', type=str, choices=[':']) - cmdline.add_argument('command', help='The command string to run for each input, containing any number of substitutions listed in the Description section', type=str) - cmdline.add_argument('-exclude', help='Exclude one specific input string / all strings matching a regular expression from being processed (see Example Usage)', action='append', metavar='"regex"', nargs=1) - cmdline.add_argument('-test', help='Test the operation of the for_each script, by printing the command strings following string substitution but not actually executing them', action='store_true', default=False) + 'By specifying the -test option, ' + 'the script will print to the terminal the results of text substitutions for all of the specified inputs, ' + 'but will not actually execute those commands. ' + 'It can therefore be used to verify that the script is receiving the intended set of inputs, ' + 'and that the text substitutions on those inputs lead to the intended command strings.') + cmdline.add_argument('inputs', + nargs='+', + help='Each of the inputs for which processing should be run') + cmdline.add_argument('colon', + type=str, + choices=[':'], + help='Colon symbol (":") delimiting the for_each inputs & command-line options from the actual command to be executed') + cmdline.add_argument('command', + type=str, + help='The command string to run for each input, ' + 'containing any number of substitutions listed in the Description section') + cmdline.add_argument('-exclude', + action='append', + metavar='"regex"', + nargs=1, + help='Exclude one specific input string / all strings matching a regular expression from being processed ' + '(see Example Usage)') + cmdline.add_argument('-test', + action='store_true', + default=False, + help='Test the operation of the for_each script, ' + 'by printing the command strings following string substitution but not actually executing them') # Usage of for_each needs to be handled slightly differently here: # We want argparse to parse only the contents of the command-line before the colon symbol, @@ -113,13 +184,13 @@ def execute(): #pylint: disable=unused-variable from mrtrix3 import app, run #pylint: disable=no-name-in-module, import-outside-toplevel inputs = app.ARGS.inputs - app.debug('All inputs: ' + str(inputs)) - app.debug('Command: ' + str(app.ARGS.command)) - app.debug('CMDSPLIT: ' + str(CMDSPLIT)) + app.debug(f'All inputs: {inputs}') + app.debug(f'Command: {app.ARGS.command}') + app.debug(f'CMDSPLIT: {CMDSPLIT}') if app.ARGS.exclude: app.ARGS.exclude = [ exclude[0] for exclude in app.ARGS.exclude ] # To deal with argparse's action=append. Always guaranteed to be only one argument since nargs=1 - app.debug('To exclude: ' + str(app.ARGS.exclude)) + app.debug(f'To exclude: {app.ARGS.exclude}') exclude_unmatched = [ ] to_exclude = [ ] for exclude in app.ARGS.exclude: @@ -134,36 +205,43 @@ def execute(): #pylint: disable=unused-variable if search_result and search_result.group(): regex_hits.append(arg) if regex_hits: - app.debug('Inputs excluded via regex "' + exclude + '": ' + str(regex_hits)) + app.debug(f'Inputs excluded via regex "{exclude}": {regex_hits}') to_exclude.extend(regex_hits) else: - app.debug('Compiled exclude regex "' + exclude + '" had no hits') + app.debug(f'Compiled exclude regex "{exclude}" had no hits') exclude_unmatched.append(exclude) except re.error: - app.debug('Exclude string "' + exclude + '" did not compile as regex') + app.debug(f'Exclude string "{exclude}" did not compile as regex') exclude_unmatched.append(exclude) if exclude_unmatched: - app.warn('Item' + ('s' if len(exclude_unmatched) > 1 else '') + ' specified via -exclude did not result in item exclusion, whether by direct match or compilation as regex: ' + str('\'' + exclude_unmatched[0] + '\'' if len(exclude_unmatched) == 1 else exclude_unmatched)) + + app.warn(f'{"Items" if len(exclude_unmatched) > 1 else "Item"} ' + 'specified via -exclude did not result in item exclusion, ' + 'whether by direct match or compilation as regex: ' + + (f'"{exclude_unmatched[0]}"' if len(exclude_unmatched) == 1 else str(exclude_unmatched))) inputs = [ arg for arg in inputs if arg not in to_exclude ] if not inputs: - raise MRtrixError('No inputs remaining after application of exclusion criteri' + ('on' if len(app.ARGS.exclude) == 1 else 'a')) - app.debug('Inputs after exclusion: ' + str(inputs)) + raise MRtrixError(f'No inputs remaining after application of exclusion {"criterion" if len(app.ARGS.exclude) == 1 else "criteria"}') + app.debug(f'Inputs after exclusion: {inputs}') common_prefix = os.path.commonprefix(inputs) common_suffix = os.path.commonprefix([i[::-1] for i in inputs])[::-1] - app.debug('Common prefix: ' + common_prefix if common_prefix else 'No common prefix') - app.debug('Common suffix: ' + common_suffix if common_suffix else 'No common suffix') + app.debug(f'Common prefix: {common_prefix}' if common_prefix else 'No common prefix') + app.debug(f'Common suffix: {common_suffix}' if common_suffix else 'No common suffix') for entry in CMDSPLIT: if os.path.exists(entry): keys_present = [ key for key in KEYLIST if key in entry ] if keys_present: - app.warn('Performing text substitution of ' + str(keys_present) + ' within command: "' + entry + '"; but the original text exists as a path on the file system... is this a problematic filesystem path?') + app.warn(f'Performing text substitution of {keys_present} within command: "{entry}"; ' + f'but the original text exists as a path on the file system... ' + f'is this a problematic filesystem path?') try: next(entry for entry in CMDSPLIT if any(key for key in KEYLIST if key in entry)) except StopIteration as exception: - raise MRtrixError('None of the unique for_each keys ' + str(KEYLIST) + ' appear in command string "' + app.ARGS.command + '"; no substitution can occur') from exception + raise MRtrixError(f'None of the unique for_each keys {KEYLIST} appear in command string "{app.ARGS.command}"; ' + f'no substitution can occur') from exception class Entry: def __init__(self, input_text): @@ -177,8 +255,8 @@ def execute(): #pylint: disable=unused-variable self.sub_uni = input_text[len(common_prefix):] self.substitutions = { 'IN': self.sub_in, 'NAME': self.sub_name, 'PRE': self.sub_pre, 'UNI': self.sub_uni } - app.debug('Input text: ' + input_text) - app.debug('Substitutions: ' + str(self.substitutions)) + app.debug(f'Input text: {input_text}') + app.debug(f'Substitutions: {self.substitutions}') self.cmd = [ ] for entry in CMDSPLIT: @@ -187,7 +265,7 @@ def execute(): #pylint: disable=unused-variable if ' ' in entry: entry = '"' + entry + '"' self.cmd.append(entry) - app.debug('Resulting command: ' + str(self.cmd)) + app.debug(f'Resulting command: {self.cmd}') self.outputtext = None self.returncode = None @@ -197,24 +275,20 @@ def execute(): #pylint: disable=unused-variable jobs.append(Entry(i)) if app.ARGS.test: - app.console('Command strings for ' + str(len(jobs)) + ' jobs:') + app.console(f'Command strings for {len(jobs)} jobs:') for job in jobs: - sys.stderr.write(ANSI.execute + 'Input:' + ANSI.clear + ' "' + job.input_text + '"\n') - sys.stderr.write(ANSI.execute + 'Command:' + ANSI.clear + ' ' + ' '.join(job.cmd) + '\n') + sys.stderr.write(ANSI.execute + 'Input:' + ANSI.clear + f' "{job.input_text}"\n') + sys.stderr.write(ANSI.execute + 'Command:' + ANSI.clear + f' {" ".join(job.cmd)}\n') return parallel = app.NUM_THREADS is not None and app.NUM_THREADS > 1 def progress_string(): - text = str(sum(1 if job.returncode is not None else 0 for job in jobs)) + \ - '/' + \ - str(len(jobs)) + \ - ' jobs completed ' + \ - ('across ' + str(app.NUM_THREADS) + ' threads' if parallel else 'sequentially') + success_count = sum(1 if job.returncode is not None else 0 for job in jobs) fail_count = sum(1 if job.returncode else 0 for job in jobs) - if fail_count: - text += ' (' + str(fail_count) + ' errors)' - return text + threading_message = f'across {app.NUM_THREADS} threads' if parallel else 'sequentially' + fail_message = f' ({fail_count} errors)' if fail_count else '' + return f'{success_count}/{len(jobs)} jobs completed {threading_message}{fail_message}' progress = app.ProgressBar(progress_string(), len(jobs)) @@ -264,35 +338,36 @@ def execute(): #pylint: disable=unused-variable assert all(job.returncode is not None for job in jobs) fail_count = sum(1 if job.returncode else 0 for job in jobs) if fail_count: - app.warn(str(fail_count) + ' of ' + str(len(jobs)) + ' jobs did not complete successfully') + app.warn(f'{fail_count} of {len(jobs)} jobs did not complete successfully') if fail_count > 1: app.warn('Outputs from failed commands:') - sys.stderr.write(app.EXEC_NAME + ':\n') + sys.stderr.write(f'{app.EXE_NAME}:\n') else: app.warn('Output from failed command:') for job in jobs: if job.returncode: if job.outputtext: - app.warn('For input "' + job.sub_in + '" (returncode = ' + str(job.returncode) + '):') + app.warn(f'For input "{job.sub_in}" (returncode = {job.returncode}):') for line in job.outputtext.splitlines(): - sys.stderr.write(' ' * (len(app.EXEC_NAME)+2) + line + '\n') + sys.stderr.write(f'{" " * (len(app.EXEC_NAME)+2)}{line}\n') else: - app.warn('No output from command for input "' + job.sub_in + '" (return code = ' + str(job.returncode) + ')') + app.warn(f'No output from command for input "{job.sub_in}" (return code = {job.returncode})') if fail_count > 1: - sys.stderr.write(app.EXEC_NAME + ':\n') - raise MRtrixError(str(fail_count) + ' of ' + str(len(jobs)) + ' jobs did not complete successfully: ' + str([job.input_text for job in jobs if job.returncode])) + sys.stderr.write(f'{app.EXE_NAME}:\n') + raise MRtrixError(f'{fail_count} of {len(jobs)} jobs did not complete successfully: ' + f'{[job.input_text for job in jobs if job.returncode]}') if app.VERBOSITY > 1: if any(job.outputtext for job in jobs): - sys.stderr.write(app.EXEC_NAME + ':\n') + sys.stderr.write('{app.EXE_NAME}:\n') for job in jobs: if job.outputtext: - app.console('Output of command for input "' + job.sub_in + '":') + app.console(f'Output of command for input "{job.sub_in}":') for line in job.outputtext.splitlines(): - sys.stderr.write(' ' * (len(app.EXEC_NAME)+2) + line + '\n') + sys.stderr.write(f'{" " * (len(app.EXEC_NAME)+2)}{line}\n') else: - app.console('No output from command for input "' + job.sub_in + '"') - sys.stderr.write(app.EXEC_NAME + ':\n') + app.console(f'No output from command for input "{job.sub_in}"') + sys.stderr.write('{app.EXE_NAME}:\n') else: app.console('No output from command for any inputs') diff --git a/bin/labelsgmfix b/bin/labelsgmfix index f7e1673621..e1b941a440 100755 --- a/bin/labelsgmfix +++ b/bin/labelsgmfix @@ -32,16 +32,40 @@ import math, os def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - cmdline.set_synopsis('In a FreeSurfer parcellation image, replace the sub-cortical grey matter structure delineations using FSL FIRST') - cmdline.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) - cmdline.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) - cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. The effects of SIFT on the reproducibility and biological accuracy of the structural connectome. NeuroImage, 2015, 104, 253-265') - cmdline.add_argument('parc', type=app.Parser.ImageIn(), help='The input FreeSurfer parcellation image') - cmdline.add_argument('t1', type=app.Parser.ImageIn(), help='The T1 image to be provided to FIRST') - cmdline.add_argument('lut', type=app.Parser.FileIn(), help='The lookup table file that the parcellated image is based on') - cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output parcellation image') - cmdline.add_argument('-premasked', action='store_true', default=False, help='Indicate that brain masking has been applied to the T1 input image') - cmdline.add_argument('-sgm_amyg_hipp', action='store_true', default=False, help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures, and also replace their estimates with those from FIRST') + cmdline.set_synopsis('In a FreeSurfer parcellation image, ' + 'replace the sub-cortical grey matter structure delineations using FSL FIRST') + cmdline.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. ' + 'A Bayesian model of shape and appearance for subcortical brain segmentation. ' + 'NeuroImage, 2011, 56, 907-922', + is_external=True) + cmdline.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. ' + 'Advances in functional and structural MR image analysis and implementation as FSL. ' + 'NeuroImage, 2004, 23, S208-S219', + is_external=True) + cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. ' + 'The effects of SIFT on the reproducibility and biological accuracy of the structural connectome. ' + 'NeuroImage, 2015, 104, 253-265') + cmdline.add_argument('parc', + type=app.Parser.ImageIn(), + help='The input FreeSurfer parcellation image') + cmdline.add_argument('t1', + type=app.Parser.ImageIn(), + help='The T1 image to be provided to FIRST') + cmdline.add_argument('lut', + type=app.Parser.FileIn(), + help='The lookup table file that the parcellated image is based on') + cmdline.add_argument('output', + type=app.Parser.ImageOut(), + help='The output parcellation image') + cmdline.add_argument('-premasked', + action='store_true', + default=False, + help='Indicate that brain masking has been applied to the T1 input image') + cmdline.add_argument('-sgm_amyg_hipp', + action='store_true', + default=False, + help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures, ' + 'and also replace their estimates with those from FIRST') @@ -55,18 +79,19 @@ def execute(): #pylint: disable=unused-variable if utils.is_windows(): raise MRtrixError('Script cannot run on Windows due to FSL dependency') - app.check_output_path(path.from_user(app.ARGS.output, False)) - image.check_3d_nonunity(path.from_user(app.ARGS.t1, False)) + image.check_3d_nonunity(app.ARGS.t1) fsl_path = os.environ.get('FSLDIR', '') if not fsl_path: - raise MRtrixError('Environment variable FSLDIR is not set; please run appropriate FSL configuration script') + raise MRtrixError('Environment variable FSLDIR is not set; ' + 'please run appropriate FSL configuration script') first_cmd = fsl.exe_name('run_first_all') first_atlas_path = os.path.join(fsl_path, 'data', 'first', 'models_336_bin') if not os.path.isdir(first_atlas_path): - raise MRtrixError('Atlases required for FSL\'s FIRST program not installed;\nPlease install fsl-first-data using your relevant package manager') + raise MRtrixError('Atlases required for FSL\'s FIRST program not installed; ' + 'please install fsl-first-data using your relevant package manager') # Want a mapping between FreeSurfer node names and FIRST structure names # Just deal with the 5 that are used in ACT; FreeSurfer's hippocampus / amygdala segmentations look good enough. @@ -79,36 +104,34 @@ def execute(): #pylint: disable=unused-variable structure_map.update({ 'L_Amyg':'Left-Amygdala', 'R_Amyg':'Right-Amygdala', 'L_Hipp':'Left-Hippocampus', 'R_Hipp':'Right-Hippocampus' }) - t1_spacing = image.Header(path.from_user(app.ARGS.t1, False)).spacing() + t1_spacing = image.Header(app.ARGS.t1).spacing() upsample_for_first = False # If voxel size is 1.25mm or larger, make a guess that the user has erroneously re-gridded their data if math.pow(t1_spacing[0] * t1_spacing[1] * t1_spacing[2], 1.0/3.0) > 1.225: - app.warn('Voxel size of input T1 image larger than expected for T1-weighted images (' + str(t1_spacing) + '); ' - 'image will be resampled to 1mm isotropic in order to maximise chance of ' - 'FSL FIRST script succeeding') + app.warn(f'Voxel size of input T1 image larger than expected for T1-weighted images ({t1_spacing}); ' + f'image will be resampled to 1mm isotropic in order to maximise chance of ' + f'FSL FIRST script succeeding') upsample_for_first = True - app.make_scratch_dir() - + app.activate_scratch_dir() # Get the parcellation and T1 images into the scratch directory, with conversion of the T1 into the correct format for FSL - run.command('mrconvert ' + path.from_user(app.ARGS.parc) + ' ' + path.to_scratch('parc.mif'), + run.command(['mrconvert', app.ARGS.parc, 'parc.mif'], preserve_pipes=True) if upsample_for_first: - run.command('mrgrid ' + path.from_user(app.ARGS.t1) + ' regrid - -voxel 1.0 -interp sinc | ' + run.command(f'mrgrid {app.ARGS.t1} regrid - -voxel 1.0 -interp sinc | ' 'mrcalc - 0.0 -max - | ' - 'mrconvert - ' + path.to_scratch('T1.nii') + ' -strides -1,+2,+3', + 'mrconvert - T1.nii -strides -1,+2,+3', preserve_pipes=True) else: - run.command('mrconvert ' + path.from_user(app.ARGS.t1) + ' ' + path.to_scratch('T1.nii') + ' -strides -1,+2,+3', + run.command(f'mrconvert {app.ARGS.t1} T1.nii -strides -1,+2,+3', preserve_pipes=True) - app.goto_scratch_dir() - # Run FIRST first_input_is_brain_extracted = '' if app.ARGS.premasked: first_input_is_brain_extracted = ' -b' - run.command(first_cmd + ' -m none -s ' + ','.join(structure_map.keys()) + ' -i T1.nii' + first_input_is_brain_extracted + ' -o first') + structures_string = ','.join(structure_map.keys()) + run.command(f'{first_cmd} -m none -s {structures_string} -i T1.nii {first_input_is_brain_extracted} -o first') fsl.check_first('first', structure_map.keys()) # Generate an empty image that will be used to construct the new SGM nodes @@ -131,14 +154,15 @@ def execute(): #pylint: disable=unused-variable mask_list = [ ] progress = app.ProgressBar('Generating mask images for SGM structures', len(structure_map)) for key, value in structure_map.items(): - image_path = key + '_mask.mif' + image_path = f'{key}_mask.mif' mask_list.append(image_path) - vtk_in_path = 'first-' + key + '_first.vtk' - run.command('meshconvert ' + vtk_in_path + ' first-' + key + '_transformed.vtk -transform first2real T1.nii') - run.command('mesh2voxel first-' + key + '_transformed.vtk parc.mif - | mrthreshold - ' + image_path + ' -abs 0.5') + vtk_in_path = f'first-{key}_first.vtk' + run.command(f'meshconvert {vtk_in_path} first-{key}_transformed.vtk -transform first2real T1.nii') + run.command(f'mesh2voxel first-{key}_transformed.vtk parc.mif - | ' + f'mrthreshold - {image_path} -abs 0.5') # Add to the SGM image; don't worry about overlap for now node_index = sgm_lut[value] - run.command('mrcalc ' + image_path + ' ' + node_index + ' sgm.mif -if sgm_new.mif') + run.command(f'mrcalc {image_path} {node_index} sgm.mif -if sgm_new.mif') if not app.CONTINUE_OPTION: run.function(os.remove, 'sgm.mif') run.function(os.rename, 'sgm_new.mif', 'sgm.mif') @@ -151,7 +175,7 @@ def execute(): #pylint: disable=unused-variable run.command('mrcalc sgm_overlap_mask.mif 0 sgm.mif -if sgm_masked.mif') # Convert the SGM label image to the indices that are required based on the user-provided LUT file - run.command('labelconvert sgm_masked.mif ' + sgm_lut_file_path + ' ' + path.from_user(app.ARGS.lut) + ' sgm_new_labels.mif') + run.command(['labelconvert', 'sgm_masked.mif', sgm_lut_file_path, app.ARGS.lut, 'sgm_new_labels.mif']) # For each SGM structure: # * Figure out what index the structure has been mapped to; this can only be done using mrstats @@ -159,9 +183,9 @@ def execute(): #pylint: disable=unused-variable # * Insert the new delineation of that structure progress = app.ProgressBar('Replacing SGM parcellations', len(structure_map)) for struct in structure_map: - image_path = struct + '_mask.mif' + image_path = f'{struct}_mask.mif' index = int(image.statistics('sgm_new_labels.mif', mask=image_path).median) - run.command('mrcalc parc.mif ' + str(index) + ' -eq 0 parc.mif -if parc_removed.mif') + run.command(f'mrcalc parc.mif {index} -eq 0 parc.mif -if parc_removed.mif') run.function(os.remove, 'parc.mif') run.function(os.rename, 'parc_removed.mif', 'parc.mif') progress.increment() @@ -170,8 +194,8 @@ def execute(): #pylint: disable=unused-variable # Insert the new delineations of all SGM structures in a single call # Enforce unsigned integer datatype of output image run.command('mrcalc sgm_new_labels.mif 0.5 -gt sgm_new_labels.mif parc.mif -if result.mif -datatype uint32') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.parc, False), + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.parc, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/bin/mask2glass b/bin/mask2glass index 56efdd8c9c..3a3e028450 100755 --- a/bin/mask2glass +++ b/bin/mask2glass @@ -17,35 +17,47 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Remika Mito (remika.mito@florey.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') + cmdline.set_author('Remika Mito (remika.mito@florey.edu.au) ' + 'and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Create a glass brain from mask input') - cmdline.add_description('The output of this command is a glass brain image, which can be viewed ' - 'using the volume render option in mrview, and used for visualisation purposes to view results in 3D.') - cmdline.add_description('While the name of this script indicates that a binary mask image is required as input, it can ' - 'also operate on a floating-point image. One way in which this can be exploited is to compute the mean ' - 'of all subject masks within template space, in which case this script will produce a smoother result ' - 'than if a binary template mask were to be used as input.') - cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input mask image') - cmdline.add_argument('output', type=app.Parser.ImageOut(), help='The output glass brain image') - cmdline.add_argument('-dilate', type=app.Parser.Int(0), default=2, help='Provide number of passes for dilation step; default = 2') - cmdline.add_argument('-scale', type=app.Parser.Float(0.0), default=2.0, help='Provide resolution upscaling value; default = 2.0') - cmdline.add_argument('-smooth', type=app.Parser.Float(0.0), default=1.0, help='Provide standard deviation of smoothing (in mm); default = 1.0') + cmdline.add_description('The output of this command is a glass brain image, ' + 'which can be viewed using the volume render option in mrview, ' + 'and used for visualisation purposes to view results in 3D.') + cmdline.add_description('While the name of this script indicates that a binary mask image is required as input, ' + 'it can also operate on a floating-point image. ' + 'One way in which this can be exploited is to compute the mean of all subject masks within template space, ' + 'in which case this script will produce a smoother result than if a binary template mask were to be used as input.') + cmdline.add_argument('input', + type=app.Parser.ImageIn(), + help='The input mask image') + cmdline.add_argument('output', + type=app.Parser.ImageOut(), + help='The output glass brain image') + cmdline.add_argument('-dilate', + type=app.Parser.Int(0), + default=2, + help='Provide number of passes for dilation step; default = 2') + cmdline.add_argument('-scale', + type=app.Parser.Float(0.0), + default=2.0, + help='Provide resolution upscaling value; default = 2.0') + cmdline.add_argument('-smooth', + type=app.Parser.Float(0.0), + default=1.0, + help='Provide standard deviation of smoothing (in mm); default = 1.0') def execute(): #pylint: disable=unused-variable - from mrtrix3 import app, image, path, run #pylint: disable=no-name-in-module, import-outside-toplevel - - app.check_output_path(app.ARGS.output) + from mrtrix3 import app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel # import data to scratch directory - app.make_scratch_dir() - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('in.mif'), + app.activate_scratch_dir() + run.command(['mrconvert', app.ARGS.input, 'in.mif'], preserve_pipes=True) - app.goto_scratch_dir() - dilate_option = ' -npass ' + str(app.ARGS.dilate) - scale_option = ' -scale ' + str(app.ARGS.scale) - smooth_option = ' -stdev ' + str(app.ARGS.smooth) + dilate_option = f' -npass {app.ARGS.dilate}' + scale_option = f' -scale {app.ARGS.scale}' + smooth_option = f' -stdev {app.ARGS.smooth}' threshold_option = ' -abs 0.5' # check whether threshold should be fixed at 0.5 or computed automatically from the data @@ -55,10 +67,12 @@ def execute(): #pylint: disable=unused-variable app.debug('Input image is not bitwise; checking distribution of image intensities') result_stat = image.statistics('in.mif') if not (result_stat.min == 0.0 and result_stat.max == 1.0): - app.warn('Input image contains values outside of range [0.0, 1.0]; threshold will not be 0.5, but will instead be determined from the image data') + app.warn('Input image contains values outside of range [0.0, 1.0]; ' + 'threshold will not be 0.5, but will instead be determined from the image data') threshold_option = '' else: - app.debug('Input image values reside within [0.0, 1.0] range; fixed threshold of 0.5 will be used') + app.debug('Input image values reside within [0.0, 1.0] range; ' + 'fixed threshold of 0.5 will be used') # run upscaling step run.command('mrgrid in.mif regrid upsampled.mif' + scale_option) @@ -76,8 +90,8 @@ def execute(): #pylint: disable=unused-variable run.command('mrcalc upsampled_smooth_thresh_dilate.mif upsampled_smooth_thresh.mif -xor out.mif -datatype bit') # create output image - run.command('mrconvert out.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'out.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/bin/mrtrix_cleanup b/bin/mrtrix_cleanup index 57ff9bbe3d..a2a7702226 100755 --- a/bin/mrtrix_cleanup +++ b/bin/mrtrix_cleanup @@ -27,12 +27,28 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Clean up residual temporary files & scratch directories from MRtrix3 commands') - cmdline.add_description('This script will search the file system at the specified location (and in sub-directories thereof) for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, and attempt to delete them.') - cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. Cleanup of such locations should instead be performed explicitly: e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') - cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: it may delete temporary items during operation that may lead to unexpected behaviour.') - cmdline.add_argument('path', type=app.Parser.DirectoryIn(), help='Directory from which to commence filesystem search') - cmdline.add_argument('-test', action='store_true', help='Run script in test mode: will list identified files / directories, but not attempt to delete them') - cmdline.add_argument('-failed', type=app.Parser.FileOut(), metavar='file', help='Write list of items that the script failed to delete to a text file') + cmdline.add_description('This script will search the file system at the specified location ' + '(and in sub-directories thereof) ' + 'for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, ' + 'and attempt to delete them.') + cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. ' + 'This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. ' + 'Cleanup of such locations should instead be performed explicitly: ' + 'e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') + cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: ' + 'it may delete temporary items during operation that may lead to unexpected behaviour.') + cmdline.add_argument('path', + type=app.Parser.DirectoryIn(), + help='Directory from which to commence filesystem search') + cmdline.add_argument('-test', + action='store_true', + help='Run script in test mode: ' + 'will list identified files / directories, ' + 'but not attempt to delete them') + cmdline.add_argument('-failed', + type=app.Parser.FileOut(), + metavar='file', + help='Write list of items that the script failed to delete to a text file') cmdline.flag_mutually_exclusive_options([ 'test', 'failed' ]) @@ -62,7 +78,7 @@ def execute(): #pylint: disable=unused-variable dirs_to_delete.extend([os.path.join(dirname, subdirname) for subdirname in items]) subdirlist[:] = list(set(subdirlist)-items) def print_msg(): - return 'Searching' + print_search_dir + ' (found ' + str(len(files_to_delete)) + ' files, ' + str(len(dirs_to_delete)) + ' directories)' + return f'Searching{print_search_dir} (found {len(files_to_delete)} files, {len(dirs_to_delete)} directories)' progress = app.ProgressBar(print_msg) for dirname, subdirlist, filelist in os.walk(root_dir): file_search(file_regex) @@ -76,19 +92,21 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.test: if files_to_delete: - app.console('Files identified (' + str(len(files_to_delete)) + '):') + app.console(f'Files identified ({len(files_to_delete)}):') for filepath in files_to_delete: - app.console(' ' + filepath) + app.console(f' {filepath}') else: - app.console('No files' + ('' if dirs_to_delete else ' or directories') + ' found') + app.console(f'No files{"" if dirs_to_delete else " or directories"} found') if dirs_to_delete: - app.console('Directories identified (' + str(len(dirs_to_delete)) + '):') + app.console(f'Directories identified ({len(dirs_to_delete)}):') for dirpath in dirs_to_delete: - app.console(' ' + dirpath) + app.console(f' {dirpath}') elif files_to_delete: app.console('No directories identified') elif files_to_delete or dirs_to_delete: - progress = app.ProgressBar('Deleting temporaries (' + str(len(files_to_delete)) + ' files, ' + str(len(dirs_to_delete)) + ' directories)', len(files_to_delete) + len(dirs_to_delete)) + progress = app.ProgressBar(f'Deleting temporaries ' + f'({len(files_to_delete)} files, {len(dirs_to_delete)} directories)', + len(files_to_delete) + len(dirs_to_delete)) except_list = [ ] size_deleted = 0 for filepath in files_to_delete: @@ -118,18 +136,21 @@ def execute(): #pylint: disable=unused-variable if postfix_index: size_deleted = round(size_deleted / math.pow(1024, postfix_index), 2) def print_freed(): - return ' (' + str(size_deleted) + POSTFIXES[postfix_index] + ' freed)' if size_deleted else '' + return f' ({size_deleted} {POSTFIXES[postfix_index]} freed)' if size_deleted else '' if except_list: - app.console(str(len(files_to_delete) + len(dirs_to_delete) - len(except_list)) + ' of ' + str(len(files_to_delete) + len(dirs_to_delete)) + ' items erased' + print_freed()) + app.console('%d of %d items erased%s' # pylint: disable=consider-using-f-string + % (len(files_to_delete) + len(dirs_to_delete) - len(except_list), + len(files_to_delete) + len(dirs_to_delete), + print_freed())) if app.ARGS.failed: with open(app.ARGS.failed, 'w', encoding='utf-8') as outfile: for item in except_list: outfile.write(item + '\n') - app.console('List of items script failed to erase written to file "' + app.ARGS.failed + '"') + app.console(f'List of items script failed to erase written to file "{app.ARGS.failed}"') else: app.console('Items that could not be erased:') for item in except_list: - app.console(' ' + item) + app.console(f' {item}') else: app.console('All items deleted successfully' + print_freed()) else: diff --git a/bin/population_template b/bin/population_template index c6d91edc65..d7c76bb820 100755 --- a/bin/population_template +++ b/bin/population_template @@ -16,7 +16,9 @@ # For more details, see http://www.mrtrix.org/. # Generates an unbiased group-average template via image registration of images to a midway space. - +# TODO Make use of pathlib throughout; should be able to remove shlex dependency +# TODO Consider asserting that anything involving image paths in this script +# be based on pathlib rather than strings import json, math, os, re, shlex, shutil, sys DEFAULT_RIGID_SCALES = [0.3,0.4,0.6,0.8,1.0,1.0] @@ -46,59 +48,225 @@ IMAGEEXT = ['mif', 'nii', 'mih', 'mgh', 'mgz', 'img', 'hdr'] def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('David Raffelt (david.raffelt@florey.edu.au) & Max Pietsch (maximilian.pietsch@kcl.ac.uk) & Thijs Dhollander (thijs.dhollander@gmail.com)') + cmdline.set_author('David Raffelt (david.raffelt@florey.edu.au) ' + '& Max Pietsch (maximilian.pietsch@kcl.ac.uk) ' + '& Thijs Dhollander (thijs.dhollander@gmail.com)') cmdline.set_synopsis('Generates an unbiased group-average template from a series of images') - cmdline.add_description('First a template is optimised with linear registration (rigid and/or affine, both by default), then non-linear registration is used to optimise the template further.') - cmdline.add_argument('input_dir', nargs='+', type=app.Parser.Various(), help='Input directory containing all images of a given contrast') - cmdline.add_argument('template', type=app.Parser.ImageOut(), help='Output template image') + cmdline.add_description('First a template is optimised with linear registration ' + '(rigid and/or affine, both by default), ' + 'then non-linear registration is used to optimise the template further.') + cmdline.add_argument('input_dir', + nargs='+', + type=app.Parser.Various(), + help='Input directory containing all images of a given contrast') + cmdline.add_argument('template', + type=app.Parser.ImageOut(), + help='Output template image') cmdline.add_example_usage('Multi-contrast registration', 'population_template input_WM_ODFs/ output_WM_template.mif input_GM_ODFs/ output_GM_template.mif', - 'When performing multi-contrast registration, the input directory and corresponding output template ' + 'When performing multi-contrast registration, ' + 'the input directory and corresponding output template ' 'image for a given contrast are to be provided as a pair, ' 'with the pairs corresponding to different contrasts provided sequentially.') options = cmdline.add_argument_group('Multi-contrast options') - options.add_argument('-mc_weight_initial_alignment', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0') - options.add_argument('-mc_weight_rigid', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_affine', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0') - options.add_argument('-mc_weight_nl', type=app.Parser.SequenceFloat(), help='Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0') + options.add_argument('-mc_weight_initial_alignment', + type=app.Parser.SequenceFloat(), + help='Weight contribution of each contrast to the initial alignment. ' + 'Comma separated, default: 1.0 for each contrast (ie. equal weighting).') + options.add_argument('-mc_weight_rigid', + type=app.Parser.SequenceFloat(), + help='Weight contribution of each contrast to the objective of rigid registration. ' + 'Comma separated, default: 1.0 for each contrast (ie. equal weighting)') + options.add_argument('-mc_weight_affine', + type=app.Parser.SequenceFloat(), + help='Weight contribution of each contrast to the objective of affine registration. ' + 'Comma separated, default: 1.0 for each contrast (ie. equal weighting)') + options.add_argument('-mc_weight_nl', + type=app.Parser.SequenceFloat(), + help='Weight contribution of each contrast to the objective of nonlinear registration. ' + 'Comma separated, default: 1.0 for each contrast (ie. equal weighting)') linoptions = cmdline.add_argument_group('Options for the linear registration') - linoptions.add_argument('-linear_no_pause', action='store_true', help='Do not pause the script if a linear registration seems implausible') - linoptions.add_argument('-linear_no_drift_correction', action='store_true', help='Deactivate correction of template appearance (scale and shear) over iterations') - linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, help='Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: |x|), l2 (ordinary least squares), lp (least powers: |x|^1.2), none (no robust estimator). Default: none.') - linoptions.add_argument('-rigid_scale', type=app.Parser.SequenceFloat(), help='Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: %s). This and affine_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_RIGID_SCALES])) - linoptions.add_argument('-rigid_lmax', type=app.Parser.SequenceInt(), help='Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_RIGID_LMAX])) - linoptions.add_argument('-rigid_niter', type=app.Parser.SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list') - linoptions.add_argument('-affine_scale', type=app.Parser.SequenceFloat(), help='Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: %s). This and rigid_scale implicitly define the number of template levels' % ','.join([str(x) for x in DEFAULT_AFFINE_SCALES])) - linoptions.add_argument('-affine_lmax', type=app.Parser.SequenceInt(), help='Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the linear_scale factor list' % ','.join([str(x) for x in DEFAULT_AFFINE_LMAX])) - linoptions.add_argument('-affine_niter', type=app.Parser.SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-linear_no_pause', + action='store_true', + help='Do not pause the script if a linear registration seems implausible') + linoptions.add_argument('-linear_no_drift_correction', + action='store_true', + help='Deactivate correction of template appearance (scale and shear) over iterations') + linoptions.add_argument('-linear_estimator', + choices=LINEAR_ESTIMATORS, + help='Specify estimator for intensity difference metric. ' + 'Valid choices are: ' + 'l1 (least absolute: |x|), ' + 'l2 (ordinary least squares), ' + 'lp (least powers: |x|^1.2), ' + 'none (no robust estimator). ' + 'Default: none.') + linoptions.add_argument('-rigid_scale', + type=app.Parser.SequenceFloat(), + help='Specify the multi-resolution pyramid used to build the rigid template, ' + 'in the form of a list of scale factors ' + f'(default: {",".join([str(x) for x in DEFAULT_RIGID_SCALES])}). ' + 'This and affine_scale implicitly define the number of template levels') + linoptions.add_argument('-rigid_lmax', + type=app.Parser.SequenceInt(), + help='Specify the lmax used for rigid registration for each scale factor, ' + 'in the form of a list of integers ' + f'(default: {",".join([str(x) for x in DEFAULT_RIGID_LMAX])}). ' + 'The list must be the same length as the linear_scale factor list') + linoptions.add_argument('-rigid_niter', + type=app.Parser.SequenceInt(), + help='Specify the number of registration iterations used within each level before updating the template, ' + 'in the form of a list of integers ' + '(default: 50 for each scale). ' + 'This must be a single number or a list of same length as the linear_scale factor list') + linoptions.add_argument('-affine_scale', + type=app.Parser.SequenceFloat(), + help='Specify the multi-resolution pyramid used to build the affine template, ' + 'in the form of a list of scale factors ' + f'(default: {",".join([str(x) for x in DEFAULT_AFFINE_SCALES])}). ' + 'This and rigid_scale implicitly define the number of template levels') + linoptions.add_argument('-affine_lmax', + type=app.Parser.SequenceInt(), + help='Specify the lmax used for affine registration for each scale factor, ' + 'in the form of a list of integers ' + f'(default: {",".join([str(x) for x in DEFAULT_AFFINE_LMAX])}). ' + 'The list must be the same length as the linear_scale factor list') + linoptions.add_argument('-affine_niter', + type=app.Parser.SequenceInt(), + help='Specify the number of registration iterations used within each level before updating the template, ' + 'in the form of a list of integers ' + '(default: 500 for each scale). ' + 'This must be a single number or a list of same length as the linear_scale factor list') nloptions = cmdline.add_argument_group('Options for the non-linear registration') - nloptions.add_argument('-nl_scale', type=app.Parser.SequenceFloat(), help='Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: %s). This implicitly defines the number of template levels' % ','.join([str(x) for x in DEFAULT_NL_SCALES])) - nloptions.add_argument('-nl_lmax', type=app.Parser.SequenceInt(), help='Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_LMAX])) - nloptions.add_argument('-nl_niter', type=app.Parser.SequenceInt(), help='Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: %s). The list must be the same length as the nl_scale factor list' % ','.join([str(x) for x in DEFAULT_NL_NITER])) - nloptions.add_argument('-nl_update_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_UPDATE_SMOOTH, help='Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_UPDATE_SMOOTH) + ' x voxel_size)') - nloptions.add_argument('-nl_disp_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_DISP_SMOOTH, help='Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default ' + str(DEFAULT_NL_DISP_SMOOTH) + ' x voxel_size)') - nloptions.add_argument('-nl_grad_step', type=app.Parser.Float(0.0), default=DEFAULT_NL_GRAD_STEP, help='The gradient step size for non-linear registration (Default: ' + str(DEFAULT_NL_GRAD_STEP) + ')') + nloptions.add_argument('-nl_scale', + type=app.Parser.SequenceFloat(), + help='Specify the multi-resolution pyramid used to build the non-linear template, ' + 'in the form of a list of scale factors ' + f'(default: {" ".join([str(x) for x in DEFAULT_NL_SCALES])}). ' + 'This implicitly defines the number of template levels') + nloptions.add_argument('-nl_lmax', + type=app.Parser.SequenceInt(), + help='Specify the lmax used for non-linear registration for each scale factor, ' + 'in the form of a list of integers ' + f'(default: {",".join([str(x) for x in DEFAULT_NL_LMAX])}). ' + 'The list must be the same length as the nl_scale factor list') + nloptions.add_argument('-nl_niter', + type=app.Parser.SequenceInt(), + help='Specify the number of registration iterations used within each level before updating the template, ' + 'in the form of a list of integers ' + f'(default: {",".join([str(x) for x in DEFAULT_NL_NITER])}). ' + 'The list must be the same length as the nl_scale factor list') + nloptions.add_argument('-nl_update_smooth', + type=app.Parser.Float(0.0), + default=DEFAULT_NL_UPDATE_SMOOTH, + help='Regularise the gradient update field with Gaussian smoothing ' + '(standard deviation in voxel units, ' + f'Default {DEFAULT_NL_UPDATE_SMOOTH} x voxel_size)') + nloptions.add_argument('-nl_disp_smooth', + type=app.Parser.Float(0.0), + default=DEFAULT_NL_DISP_SMOOTH, + help='Regularise the displacement field with Gaussian smoothing ' + '(standard deviation in voxel units, ' + f'Default {DEFAULT_NL_DISP_SMOOTH} x voxel_size)') + nloptions.add_argument('-nl_grad_step', + type=app.Parser.Float(0.0), + default=DEFAULT_NL_GRAD_STEP, + help='The gradient step size for non-linear registration ' + f'(Default: {DEFAULT_NL_GRAD_STEP})') options = cmdline.add_argument_group('Input, output and general options') - options.add_argument('-type', choices=REGISTRATION_MODES, help='Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: %s. Default: rigid_affine_nonlinear' % ', '.join('"' + x + '"' for x in REGISTRATION_MODES if "_" in x), default='rigid_affine_nonlinear') - options.add_argument('-voxel_size', type=app.Parser.SequenceFloat(), help='Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values.') - options.add_argument('-initial_alignment', choices=INITIAL_ALIGNMENT, default='mass', help='Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none".') - options.add_argument('-mask_dir', type=app.Parser.DirectoryIn(), help='Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images.') - options.add_argument('-warp_dir', type=app.Parser.DirectoryOut(), help='Output a directory containing warps from each input to the template. If the folder does not exist it will be created') - options.add_argument('-transformed_dir', type=app.Parser.DirectoryOut(), help='Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories.') - options.add_argument('-linear_transformations_dir', type=app.Parser.DirectoryOut(), help='Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created') - options.add_argument('-template_mask', type=app.Parser.ImageOut(), help='Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space.') - options.add_argument('-noreorientation', action='store_true', help='Turn off FOD reorientation in mrregister. Reorientation is on by default if the number of volumes in the 4th dimension corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series (i.e. 6, 15, 28, 45, 66 etc)') - options.add_argument('-leave_one_out', choices=LEAVE_ONE_OUT, default='auto', help='Register each input image to a template that does not contain that image. Valid choices: ' + ', '.join(LEAVE_ONE_OUT) + '. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) ') - options.add_argument('-aggregate', choices=AGGREGATION_MODES, help='Measure used to aggregate information from transformed images to the template image. Valid choices: %s. Default: mean' % ', '.join(AGGREGATION_MODES)) - options.add_argument('-aggregation_weights', type=app.Parser.FileIn(), help='Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape).') - options.add_argument('-nanmask', action='store_true', help='Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. Only works if -mask_dir has been input.') - options.add_argument('-copy_input', action='store_true', help='Copy input images and masks into local scratch directory.') - options.add_argument('-delete_temporary_files', action='store_true', help='Delete temporary files from scratch directory during template creation.') + registration_modes_string = ', '.join(f'"{x}"' for x in REGISTRATION_MODES if '_' in x) + options.add_argument('-type', + choices=REGISTRATION_MODES, + help='Specify the types of registration stages to perform. ' + 'Options are: ' + '"rigid" (perform rigid registration only, ' + 'which might be useful for intra-subject registration in longitudinal analysis), ' + '"affine" (perform affine registration), ' + '"nonlinear", ' + f'as well as cominations of registration types: {registration_modes_string}. ' + 'Default: rigid_affine_nonlinear', + default='rigid_affine_nonlinear') + options.add_argument('-voxel_size', + type=app.Parser.SequenceFloat(), + help='Define the template voxel size in mm. ' + 'Use either a single value for isotropic voxels or 3 comma-separated values.') + options.add_argument('-initial_alignment', + choices=INITIAL_ALIGNMENT, + default='mass', + help='Method of alignment to form the initial template. ' + 'Options are: ' + '"mass" (default), ' + '"robust_mass" (requires masks), ' + '"geometric", ' + '"none".') + options.add_argument('-mask_dir', + type=app.Parser.DirectoryIn(), + help='Optionally input a set of masks inside a single directory, ' + 'one per input image ' + '(with the same file name prefix). ' + 'Using masks will speed up registration significantly. ' + 'Note that masks are used for registration, ' + 'not for aggregation. ' + 'To exclude areas from aggregation, ' + 'NaN-mask your input images.') + options.add_argument('-warp_dir', + type=app.Parser.DirectoryOut(), + help='Output a directory containing warps from each input to the template. ' + 'If the folder does not exist it will be created') + # TODO Would prefer for this to be exclusively a directory; + # but to do so will need to provide some form of disambiguation of multi-contrast files + options.add_argument('-transformed_dir', + type=app.Parser.DirectoryOut(), + help='Output a directory containing the input images transformed to the template. ' + 'If the folder does not exist it will be created. ' + 'For multi-contrast registration, ' + 'this path will contain a sub-directory for the images per contrast.') + options.add_argument('-linear_transformations_dir', + type=app.Parser.DirectoryOut(), + help='Output a directory containing the linear transformations used to generate the template. ' + 'If the folder does not exist it will be created') + options.add_argument('-template_mask', + type=app.Parser.ImageOut(), + help='Output a template mask. ' + 'Only works if -mask_dir has been input. ' + 'The template mask is computed as the intersection of all subject masks in template space.') + options.add_argument('-noreorientation', + action='store_true', + help='Turn off FOD reorientation in mrregister. ' + 'Reorientation is on by default if the number of volumes in the 4th dimension ' + 'corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series ' + '(i.e. 6, 15, 28, 45, 66 etc)') + options.add_argument('-leave_one_out', + choices=LEAVE_ONE_OUT, + default='auto', + help='Register each input image to a template that does not contain that image. ' + f'Valid choices: {", ".join(LEAVE_ONE_OUT)}. ' + '(Default: auto (true if n_subjects larger than 2 and smaller than 15))') + options.add_argument('-aggregate', + choices=AGGREGATION_MODES, + help='Measure used to aggregate information from transformed images to the template image. ' + f'Valid choices: {", ".join(AGGREGATION_MODES)}. ' + 'Default: mean') + options.add_argument('-aggregation_weights', + type=app.Parser.FileIn(), + help='Comma-separated file containing weights used for weighted image aggregation. ' + 'Each row must contain the identifiers of the input image and its weight. ' + 'Note that this weighs intensity values not transformations (shape).') + options.add_argument('-nanmask', + action='store_true', + help='Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. ' + 'Only works if -mask_dir has been input.') + options.add_argument('-copy_input', + action='store_true', + help='Copy input images and masks into local scratch directory.') + options.add_argument('-delete_temporary_files', + action='store_true', + help='Delete temporary files from scratch directory during template creation.') # ENH: add option to initialise warps / transformations @@ -134,24 +302,24 @@ def check_linear_transformation(transformation, cmd, max_scaling=0.5, max_shear= max_rot = 2 * math.pi good = True - run.command('transformcalc ' + transformation + ' decompose ' + transformation + 'decomp') - if not os.path.isfile(transformation + 'decomp'): # does not exist if run with -continue option - app.console(transformation + 'decomp not found. skipping check') + run.command(f'transformcalc {transformation} decompose {transformation}decomp') + if not os.path.isfile(f'{transformation}decomp'): # does not exist if run with -continue option + app.console(f'"{transformation}decomp" not found; skipping check') return True - data = utils.load_keyval(transformation + 'decomp') - run.function(os.remove, transformation + 'decomp') + data = utils.load_keyval(f'{transformation}decomp') + run.function(os.remove, f'{transformation}_decomp') scaling = [float(value) for value in data['scaling']] if any(a < 0 for a in scaling) or any(a > (1 + max_scaling) for a in scaling) or any( a < (1 - max_scaling) for a in scaling): - app.warn("large scaling (" + str(scaling) + ") in " + transformation) + app.warn(f'large scaling ({scaling})) in {transformation}') good = False shear = [float(value) for value in data['shear']] if any(abs(a) > max_shear for a in shear): - app.warn("large shear (" + str(shear) + ") in " + transformation) + app.warn(f'large shear ({shear}) in {transformation}') good = False rot_angle = float(data['angle_axis'][0]) if abs(rot_angle) > max_rot: - app.warn("large rotation (" + str(rot_angle) + ") in " + transformation) + app.warn(f'large rotation ({rot_angle}) in {transformation}') good = False if not good: @@ -175,21 +343,21 @@ def check_linear_transformation(transformation, cmd, max_scaling=0.5, max_shear= assert what != 'affine' what = 'rigid' newcmd.append(element) - newcmd = " ".join(newcmd) + newcmd = ' '.join(newcmd) if not init_rotation_found: - app.console("replacing the transformation obtained with:") + app.console('replacing the transformation obtained with:') app.console(cmd) if what: - newcmd += ' -' + what + '_init_translation mass -' + what + '_init_rotation search' + newcmd += f' -{what}_init_translation mass -{what}_init_rotation search' app.console("by the one obtained with:") app.console(newcmd) run.command(newcmd, force=True) return check_linear_transformation(transformation, newcmd, max_scaling, max_shear, max_rot, pause_on_warn=pause_on_warn) if pause_on_warn: - app.warn("you might want to manually repeat mrregister with different parameters and overwrite the transformation file: \n%s" % transformation) - app.console('The command that failed the test was: \n' + cmd) - app.console('Working directory: \n' + os.getcwd()) - input("press enter to continue population_template") + app.warn('you might want to manually repeat mrregister with different parameters and overwrite the transformation file: \n{transformation}') + app.console(f'The command that failed the test was: \n{cmd}') + app.console(f'Working directory: \n{os.getcwd()}') + input('press enter to continue population_template') return good @@ -207,14 +375,14 @@ def aggregate(inputs, output, contrast_idx, mode, force=True): wsum = sum(float(w) for w in weights) cmd = ['mrcalc'] if wsum <= 0: - raise MRtrixError("the sum of aggregetion weights has to be positive") + raise MRtrixError('the sum of aggregetion weights has to be positive') for weight, image in zip(weights, images): if float(weight) != 0: cmd += [image, weight, '-mult'] + (['-add'] if len(cmd) > 1 else []) - cmd += ['%.16f' % wsum, '-div', output] + cmd += [f'{wsum:.16f}', '-div', output] run.command(cmd, force=force) else: - raise MRtrixError("aggregation mode %s not understood" % mode) + raise MRtrixError(f'aggregation mode {mode} not understood') def inplace_nan_mask(images, masks): @@ -222,8 +390,8 @@ def inplace_nan_mask(images, masks): assert len(images) == len(masks), (len(images), len(masks)) for image, mask in zip(images, masks): target_dir = os.path.split(image)[0] - masked = os.path.join(target_dir, '__' + os.path.split(image)[1]) - run.command("mrcalc " + mask + " " + image + " nan -if " + masked, force=True) + masked = os.path.join(target_dir, f'__{os.path.split(image)[1]}') + run.command(f'mrcalc {mask} {image} nan -if {masked}', force=True) run.function(shutil.move, masked, image) @@ -233,18 +401,19 @@ def calculate_isfinite(inputs, contrasts): for cid in range(contrasts.n_contrasts): for inp in inputs: if contrasts.n_volumes[cid] > 0: - cmd = 'mrconvert ' + inp.ims_transformed[cid] + ' -coord 3 0 - | mrcalc - -finite' + cmd = f'mrconvert {inp.ims_transformed[cid]} -coord 3 0 - | ' \ + f'mrcalc - -finite' else: - cmd = 'mrcalc ' + inp.ims_transformed[cid] + ' -finite' + cmd = f'mrcalc {inp.ims_transformed[cid]} -finite' if inp.aggregation_weight: - cmd += ' %s -mult ' % inp.aggregation_weight - cmd += ' isfinite%s/%s.mif' % (contrasts.suff[cid], inp.uid) + cmd += f' {inp.aggregation_weight} -mult' + cmd += f' isfinite{contrasts.suff[cid]}/{inp.uid}.mif' run.command(cmd, force=True) for cid in range(contrasts.n_contrasts): - cmd = ['mrmath', path.all_in_dir('isfinite%s' % contrasts.suff[cid]), 'sum'] + cmd = ['mrmath', path.all_in_dir(f'isfinite{contrasts.suff[cid]}'), 'sum'] if agg_weights: - agg_weight_norm = str(float(len(agg_weights)) / sum(agg_weights)) - cmd += ['-', '|', 'mrcalc', '-', agg_weight_norm, '-mult'] + agg_weight_norm = float(len(agg_weights)) / sum(agg_weights) + cmd += ['-', '|', 'mrcalc', '-', str(agg_weight_norm), '-mult'] run.command(cmd + [contrasts.isfinite_count[cid]], force=True) @@ -256,6 +425,7 @@ def get_common_prefix(file_list): return os.path.commonprefix(file_list) +# Todo Create singular "Contrast" class class Contrasts: """ Class that parses arguments and holds information specific to each image contrast @@ -293,15 +463,15 @@ class Contrasts: """ + def __init__(self): - from mrtrix3 import MRtrixError, path, app # pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import MRtrixError, app # pylint: disable=no-name-in-module, import-outside-toplevel n_contrasts = len(app.ARGS.input_dir) - - self.suff = ["_c" + c for c in map(str, range(n_contrasts))] + self.suff = [f'_c{c}' for c in map(str, range(n_contrasts))] self.names = [os.path.relpath(f, os.path.commonprefix(app.ARGS.input_dir)) for f in app.ARGS.input_dir] - self.templates_out = [path.from_user(t, True) for t in app.ARGS.template] + self.templates_out = [t for t in app.ARGS.template] self.mc_weight_initial_alignment = [None for _ in range(self.n_contrasts)] self.mc_weight_rigid = [None for _ in range(self.n_contrasts)] @@ -312,37 +482,38 @@ class Contrasts: self.affine_weight_option = [None for _ in range(self.n_contrasts)] self.nl_weight_option = [None for _ in range(self.n_contrasts)] - self.isfinite_count = ['isfinite' + c + '.mif' for c in self.suff] + self.isfinite_count = [f'isfinite{c}.mif' for c in self.suff] self.templates = [None for _ in range(self.n_contrasts)] self.n_volumes = [None for _ in range(self.n_contrasts)] self.fod_reorientation = [None for _ in range(self.n_contrasts)] for mode in ['initial_alignment', 'rigid', 'affine', 'nl']: - opt = app.ARGS.__dict__.get('mc_weight_' + mode, None) + opt = app.ARGS.__dict__.get(f'mc_weight_{mode}', None) if opt: if n_contrasts == 1: - raise MRtrixError('mc_weight_' + mode+' requires multiple input contrasts') - opt = opt.split(',') + raise MRtrixError(f'mc_weight_{mode} requires multiple input contrasts') if len(opt) != n_contrasts: - raise MRtrixError('mc_weight_' + mode+' needs to be defined for each contrast') + raise MRtrixError(f'mc_weight_{mode} needs to be defined for each contrast') else: - opt = ["1"] * n_contrasts - self.__dict__['mc_weight_%s' % mode] = opt - self.__dict__['%s_weight_option' % mode] = ' -mc_weights '+','.join(str(item) for item in opt)+' ' if n_contrasts > 1 else '' + opt = [1.0] * n_contrasts + self.__dict__[f'mc_weight_{mode}'] = opt + self.__dict__[f'{mode}_weight_option'] = f' -mc_weights {",".join(map(str, opt))}' if n_contrasts > 1 else '' - if len(self.templates_out) != n_contrasts: - raise MRtrixError('number of templates (%i) does not match number of input directories (%i)' % - (len(self.templates_out), n_contrasts)) + if len(app.ARGS.template) != n_contrasts: + raise MRtrixError(f'number of templates ({len(app.ARGS.template)}) ' + f'does not match number of input directories ({n_contrasts})') @property def n_contrasts(self): return len(self.suff) + # TODO Obey expected formatting of __repr__() + # (or just remove) def __repr__(self, *args, **kwargs): text = '' for cid in range(self.n_contrasts): - text += '\tcontrast: %s, template: %s, suffix: %s\n' % (self.names[cid], self.templates_out[cid], self.suff[cid]) + text += f'\tcontrast: {self.names[cid]}, suffix: {self.suff[cid]}\n' return text @@ -399,7 +570,7 @@ class Input: self.uid = uid assert self.uid, "UID empty" - assert self.uid.count(' ') == 0, 'UID "%s" contains whitespace' % self.uid + assert self.uid.count(' ') == 0, f'UID "{self.uid}" contains whitespace' assert len(directories) == len(filenames) self.ims_filenames = filenames @@ -410,8 +581,8 @@ class Input: n_contrasts = len(contrasts) - self.ims_transformed = [os.path.join('input_transformed'+contrasts[cid], uid + '.mif') for cid in range(n_contrasts)] - self.msk_transformed = os.path.join('mask_transformed', uid + '.mif') + self.ims_transformed = [os.path.join(f'input_transformed{contrasts[cid]}', f'{uid}.mif') for cid in range(n_contrasts)] + self.msk_transformed = os.path.join('mask_transformed', f'{uid}.mif') self.aggregation_weight = None @@ -421,48 +592,54 @@ class Input: def __repr__(self, *args, **kwargs): text = '\nInput [' for key in sorted([k for k in self.__dict__ if not k.startswith('_')]): - text += '\n\t' + str(key) + ': ' + str(self.__dict__[key]) + text += f'\n\t{key}: {self.__dict__[key]}' text += '\n]' return text def info(self): - message = ['input: ' + self.uid] + message = [f'input: {self.uid}'] if self.aggregation_weight: - message += ['agg weight: ' + self.aggregation_weight] + message += [f'agg weight: {self.aggregation_weight}'] for csuff, fname in zip(self.contrasts, self.ims_filenames): - message += [((csuff + ': ') if csuff else '') + '"' + fname + '"'] + message += [f'{(csuff + ": ") if csuff else ""}: "{fname}"'] if self.msk_filename: - message += ['mask: ' + self.msk_filename] + message += [f'mask: {self.msk_filename}'] return ', '.join(message) def cache_local(self): from mrtrix3 import run, path # pylint: disable=no-name-in-module, import-outside-toplevel contrasts = self.contrasts for cid, csuff in enumerate(contrasts): - if not os.path.isdir('input' + csuff): - path.make_dir('input' + csuff) - run.command('mrconvert ' + self.ims_path[cid] + ' ' + os.path.join('input' + csuff, self.uid + '.mif')) - self._local_ims = [os.path.join('input' + csuff, self.uid + '.mif') for csuff in contrasts] + if not os.path.isdir(f'input{csuff}'): + path.make_dir(f'input{csuff}') + run.command(['mrconvert', self.ims_path[cid], os.path.join(f'input{csuff}', f'{self.uid}.mif')]) + self._local_ims = [os.path.join(f'input{csuff}', f'{self.uid}.mif') for csuff in contrasts] if self.msk_filename: if not os.path.isdir('mask'): path.make_dir('mask') - run.command('mrconvert ' + self.msk_path + ' ' + os.path.join('mask', self.uid + '.mif')) - self._local_msk = os.path.join('mask', self.uid + '.mif') + run.command(['mrconvert', self.msk_path, os.path.join('mask', f'{self.uid}.mif')]) + self._local_msk = os.path.join('mask', f'{self.uid}.mif') def get_ims_path(self, quoted=True): """ return path to input images """ - from mrtrix3 import path # pylint: disable=no-name-in-module, import-outside-toplevel if self._local_ims: return self._local_ims - return [path.from_user(abspath(d, f), quoted) for d, f in zip(self._im_directories, self.ims_filenames)] + return [(shlex.quote(abspath(d, f)) \ + if quoted \ + else abspath(d, f)) \ + for d, f in zip(self._im_directories, self.ims_filenames)] ims_path = property(get_ims_path) def get_msk_path(self, quoted=True): """ return path to input mask """ - from mrtrix3 import path # pylint: disable=no-name-in-module, import-outside-toplevel if self._local_msk: return self._local_msk - return path.from_user(os.path.join(self._msk_directory, self.msk_filename), quoted) if self.msk_filename else None + if not self.msk_filename: + return None + unquoted_path = os.path.join(self._msk_directory, self.msk_filename) + if quoted: + return shlex.quote(unquoted_path) + return unquoted_path msk_path = property(get_msk_path) @@ -484,7 +661,7 @@ def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whites TODO check if no common grid & trafo across contrasts (only relevant for robust init?) """ - from mrtrix3 import MRtrixError, app, path, image # pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import MRtrixError, app, image # pylint: disable=no-name-in-module, import-outside-toplevel contrasts = contrasts.suff inputs = [] def paths_to_file_uids(paths, prefix, postfix): @@ -495,12 +672,13 @@ def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whites uid = re.sub(re.escape(postfix)+'$', '', re.sub('^'+re.escape(prefix), '', os.path.split(path)[1])) uid = re.sub(r'\s+', whitespace_repl, uid) if not uid: - raise MRtrixError('No uniquely identifiable part of filename "' + path + '" ' + raise MRtrixError(f'No uniquely identifiable part of filename "{path}" ' 'after prefix and postfix substitution ' - 'with prefix "' + prefix + '" and postfix "' + postfix + '"') - app.debug('UID mapping: "' + path + '" --> "' + uid + '"') + 'with prefix "{prefix}" and postfix "{postfix}"') + app.debug(f'UID mapping: "{path}" --> "{uid}"') if uid in uid_path: - raise MRtrixError('unique file identifier is not unique: "' + uid + '" mapped to "' + path + '" and "' + uid_path[uid] +'"') + raise MRtrixError(f'unique file identifier is not unique: ' + f'"{uid}" mapped to "{path}" and "{uid_path[uid]}"') uid_path[uid] = path uids.append(uid) return uids @@ -514,7 +692,7 @@ def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whites mask_common_prefix = get_common_prefix([os.path.split(m)[1] for m in mask_files]) mask_uids = paths_to_file_uids(mask_files, mask_common_prefix, mask_common_postfix) if app.VERBOSITY > 1: - app.console('mask uids:' + str(mask_uids)) + app.console(f'mask uids: {mask_uids}') # images uids common_postfix = [get_common_postfix(files) for files in in_files] @@ -524,41 +702,43 @@ def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whites xcontrast_xsubject_pre_postfix = [get_common_postfix(common_prefix).lstrip('_-'), get_common_prefix([re.sub('.('+'|'.join(IMAGEEXT)+')(.gz)?$', '', pfix).rstrip('_-') for pfix in common_postfix])] if app.VERBOSITY > 1: - app.console("common_postfix: " + str(common_postfix)) - app.console("common_prefix: " + str(common_prefix)) - app.console("xcontrast_xsubject_pre_postfix: " + str(xcontrast_xsubject_pre_postfix)) + app.console(f'common_postfix: {common_postfix}') + app.console(f'common_prefix: {common_prefix}') + app.console(f'xcontrast_xsubject_pre_postfix: {xcontrast_xsubject_pre_postfix}') for ipostfix, postfix in enumerate(common_postfix): if not postfix: - raise MRtrixError('image filenames do not have a common postfix:\n' + '\n'.join(in_files[ipostfix])) + raise MRtrixError('image filenames do not have a common postfix:\n%s' % '\n'.join(in_files[ipostfix])) c_uids = [] for cid, files in enumerate(in_files): c_uids.append(paths_to_file_uids(files, common_prefix[cid], common_postfix[cid])) if app.VERBOSITY > 1: - app.console('uids by contrast:' + str(c_uids)) + app.console(f'uids by contrast: {c_uids}') # join images and masks for ifile, fname in enumerate(in_files[0]): uid = c_uids[0][ifile] fnames = [fname] - dirs = [abspath(path.from_user(app.ARGS.input_dir[0], False))] + dirs = [app.ARGS.input_dir[0]] if len(contrasts) > 1: for cid in range(1, len(contrasts)): - dirs.append(abspath(path.from_user(app.ARGS.input_dir[cid], False))) + dirs.append(app.ARGS.input_dir[cid]) image.check_3d_nonunity(os.path.join(dirs[cid], in_files[cid][ifile])) if uid != c_uids[cid][ifile]: - raise MRtrixError('no matching image was found for image %s and contrasts %s and %s.' % (fname, dirs[0], dirs[cid])) + raise MRtrixError(f'no matching image was found for image {fname} and contrasts {dirs[0]} and {dirs[cid]}') fnames.append(in_files[cid][ifile]) if mask_files: if uid not in mask_uids: - raise MRtrixError('no matching mask image was found for input image ' + fname + ' with uid "'+uid+'". ' - 'Mask uid candidates: ' + ', '.join(['"%s"' % m for m in mask_uids])) + candidates_string = ', '.join([f'"{m}"' for m in mask_uids]) + raise MRtrixError(f'No matching mask image was found for input image {fname} with uid "{uid}". ' + f'Mask uid candidates: {candidates_string}') index = mask_uids.index(uid) # uid, filenames, directories, contrasts, mask_filename = '', mask_directory = '', agg_weight = None inputs.append(Input(uid, fnames, dirs, contrasts, - mask_filename=mask_files[index], mask_directory=abspath(path.from_user(app.ARGS.mask_dir, False)))) + mask_filename=mask_files[index], + mask_directory=app.ARGS.mask_dir)) else: inputs.append(Input(uid, fnames, dirs, contrasts)) @@ -579,14 +759,14 @@ def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whites for inp in inputs: if inp.uid not in agg_weights: - raise MRtrixError('aggregation weight not found for %s' % inp.uid) + raise MRtrixError(f'aggregation weight not found for {inp.uid}') inp.aggregation_weight = agg_weights[inp.uid] - app.console('Using aggregation weights ' + f_agg_weight) + app.console(f'Using aggregation weights {f_agg_weight}') weights = [float(inp.aggregation_weight) for inp in inputs if inp.aggregation_weight is not None] if sum(weights) <= 0: - raise MRtrixError('Sum of aggregation weights is not positive: ' + str(weights)) + raise MRtrixError(f'Sum of aggregation weights is not positive: {weights}') if any(w < 0 for w in weights): - app.warn('Negative aggregation weights: ' + str(weights)) + app.warn(f'Negative aggregation weights: {weights}') return inputs, xcontrast_xsubject_pre_postfix @@ -594,77 +774,82 @@ def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whites def execute(): #pylint: disable=unused-variable from mrtrix3 import MRtrixError, app, image, matrix, path, run, EXE_LIST #pylint: disable=no-name-in-module, import-outside-toplevel - expected_commands = ['mrgrid', 'mrregister', 'mrtransform', 'mraverageheader', 'mrconvert', 'mrmath', 'transformcalc', 'mrfilter'] + expected_commands = ['mrfilter', 'mrgrid', 'mrregister', 'mrtransform', 'mraverageheader', 'mrconvert', 'mrmath', 'transformcalc'] for cmd in expected_commands: if cmd not in EXE_LIST : - raise MRtrixError("Could not find " + cmd + " in bin/. Binary commands not compiled?") + raise MRtrixError(f'Could not find "{cmd}" in bin/; binary commands not compiled?') if not app.ARGS.type in REGISTRATION_MODES: - raise MRtrixError("registration type must be one of %s. provided: %s" % (str(REGISTRATION_MODES), app.ARGS.type)) - dorigid = "rigid" in app.ARGS.type - doaffine = "affine" in app.ARGS.type + raise MRtrixError(f'Registration type must be one of {REGISTRATION_MODES}; provided: "{app.ARGS.type}"') + dorigid = 'rigid' in app.ARGS.type + doaffine = 'affine' in app.ARGS.type dolinear = dorigid or doaffine - dononlinear = "nonlinear" in app.ARGS.type - assert (dorigid + doaffine + dononlinear >= 1), "FIXME: registration type not valid" + dononlinear = 'nonlinear' in app.ARGS.type + assert (dorigid + doaffine + dononlinear >= 1), 'FIXME: registration type not valid' input_output = app.ARGS.input_dir + [app.ARGS.template] n_contrasts = len(input_output) // 2 if len(input_output) != 2 * n_contrasts: - raise MRtrixError('expected two arguments per contrast, received %i: %s' % (len(input_output), ', '.join(input_output))) + raise MRtrixError(f'Expected two arguments per contrast, received {len(input_output)}: {", ".join(input_output)}') if n_contrasts > 1: app.console('Generating population template using multi-contrast registration') # reorder arguments for multi-contrast registration as after command line parsing app.ARGS.input_dir holds all but one argument + # TODO Write these to new variables rather than overwring app.ARGS? + # Or maybe better, invoke the appropriate typed arguments for each app.ARGS.input_dir = [] app.ARGS.template = [] for i_contrast in range(n_contrasts): inargs = (input_output[i_contrast*2], input_output[i_contrast*2+1]) - if not os.path.isdir(inargs[0]): - raise MRtrixError('input directory %s not found' % inargs[0]) - app.ARGS.input_dir.append(relpath(inargs[0])) - app.ARGS.template.append(relpath(inargs[1])) + app.ARGS.input_dir.append(app.Parser.DirectoryIn(inargs[0])) + app.ARGS.template.append(app.Parser.ImageOut(inargs[1])) + # Perform checks that otherwise would have been done immediately after command-line parsing + # were it not for the inability to represent input-output pairs in the command-line interface representation + for output_path in app.ARGS.template: + output_path.check_output() cns = Contrasts() app.debug(str(cns)) in_files = [sorted(path.all_in_dir(input_dir, dir_path=False)) for input_dir in app.ARGS.input_dir] if len(in_files[0]) <= 1: - raise MRtrixError('Not enough images found in input directory ' + app.ARGS.input_dir[0] + - '. More than one image is needed to generate a population template') + raise MRtrixError(f'Not enough images found in input directory {app.ARGS.input_dir[0]}; ' + 'more than one image is needed to generate a population template') if n_contrasts > 1: for cid in range(1, n_contrasts): if len(in_files[cid]) != len(in_files[0]): - raise MRtrixError('Found %i images in input directory %s ' % (len(app.ARGS.input_dir[0]), app.ARGS.input_dir[0]) + - 'but %i input images in %s.' % (len(app.ARGS.input_dir[cid]), app.ARGS.input_dir[cid])) + raise MRtrixError(f'Found {len(app.ARGS.input_dir[0])} images in input directory {app.ARGS.input_dir[0]} ' + f'but {len(app.ARGS.input_dir[cid])} input images in {app.ARGS.input_dir[cid]}') else: - app.console('Generating a population-average template from ' + str(len(in_files[0])) + ' input images') + app.console(f'Generating a population-average template from {len(in_files[0])} input images') if n_contrasts > 1: - app.console('using ' + str(len(in_files)) + ' contrasts for each input image') + app.console(f'using {len(in_files)} contrasts for each input image') voxel_size = None if app.ARGS.voxel_size: voxel_size = app.ARGS.voxel_size if len(voxel_size) not in [1, 3]: - raise MRtrixError('Voxel size needs to be a single or three comma-separated floating point numbers; received: ' + ','.join(str(item) for item in voxel_size)) + raise MRtrixError('Voxel size needs to be a single or three comma-separated floating point numbers; ' + f'received: {",".join(map(str, voxel_size))}') agg_measure = 'mean' if app.ARGS.aggregate is not None: if not app.ARGS.aggregate in AGGREGATION_MODES: - app.error("aggregation type must be one of %s. provided: %s" % (str(AGGREGATION_MODES), app.ARGS.aggregate)) + app.error(f'aggregation type must be one of {AGGREGATION_MODES}; provided: {app.ARGS.aggregate}') agg_measure = app.ARGS.aggregate agg_weights = app.ARGS.aggregation_weights if agg_weights is not None: - agg_measure = "weighted_" + agg_measure + agg_measure = 'weighted_' + agg_measure if agg_measure != 'weighted_mean': - app.error("aggregation weights require '-aggregate mean' option. provided: %s" % (app.ARGS.aggregate)) + app.error(f'aggregation weights require "-aggregate mean" option; provided: {app.ARGS.aggregate}') if not os.path.isfile(app.ARGS.aggregation_weights): - app.error("aggregation weights file not found: %s" % app.ARGS.aggregation_weights) + app.error(f'aggregation weights file not found: {app.ARGS.aggregation_weights}') initial_alignment = app.ARGS.initial_alignment - if initial_alignment not in ["mass", "robust_mass", "geometric", "none"]: - raise MRtrixError('initial_alignment must be one of ' + " ".join(["mass", "robust_mass", "geometric", "none"]) + " provided: " + str(initial_alignment)) + if initial_alignment not in INITIAL_ALIGNMENT: + raise MRtrixError(f'initial_alignment must be one of {INITIAL_ALIGNMENT}; provided: {initial_alignment}') linear_estimator = app.ARGS.linear_estimator if linear_estimator is not None and not dolinear: @@ -676,38 +861,38 @@ def execute(): #pylint: disable=unused-variable use_masks = True app.ARGS.mask_dir = relpath(app.ARGS.mask_dir) if not os.path.isdir(app.ARGS.mask_dir): - raise MRtrixError('mask directory not found') + raise MRtrixError('Mask directory not found') mask_files = sorted(path.all_in_dir(app.ARGS.mask_dir, dir_path=False)) if len(mask_files) < len(in_files[0]): - raise MRtrixError('there are not enough mask images for the number of images in the input directory') + raise MRtrixError('There are not enough mask images for the number of images in the input directory') if not use_masks: - app.warn('no masks input. Use input masks to reduce computation time and improve robustness') + app.warn('No masks input; use of input masks is recommended to reduce computation time and improve robustness') if app.ARGS.template_mask and not use_masks: - raise MRtrixError('you cannot output a template mask because no subject masks were input using -mask_dir') + raise MRtrixError('You cannot output a template mask because no subject masks were input using -mask_dir') nanmask_input = app.ARGS.nanmask if nanmask_input and not use_masks: - raise MRtrixError('you cannot use NaN masking when no subject masks were input using -mask_dir') + raise MRtrixError('You cannot use NaN masking when no subject masks were input using -mask_dir') ins, xcontrast_xsubject_pre_postfix = parse_input_files(in_files, mask_files, cns, agg_weights) leave_one_out = 'auto' if app.ARGS.leave_one_out is not None: leave_one_out = app.ARGS.leave_one_out - if not leave_one_out in ['0', '1', 'auto']: - raise MRtrixError('leave_one_out not understood: ' + str(leave_one_out)) + if not leave_one_out in LEAVE_ONE_OUT: + raise MRtrixError(f'Input to -leave_one_out not understood: {leave_one_out}') if leave_one_out == 'auto': leave_one_out = 2 < len(ins) < 15 else: leave_one_out = bool(int(leave_one_out)) if leave_one_out: - app.console('performing leave-one-out registration') + app.console('Performing leave-one-out registration') # check that at sum of weights is positive for any grouping if weighted aggregation is used weights = [float(inp.aggregation_weight) for inp in ins if inp.aggregation_weight is not None] if weights and sum(weights) - max(weights) <= 0: - raise MRtrixError('leave-one-out registration requires positive aggregation weights in all groupings') + raise MRtrixError('Leave-one-out registration requires positive aggregation weights in all groupings') noreorientation = app.ARGS.noreorientation @@ -718,27 +903,23 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError("linear option set when no linear registration is performed") if len(app.ARGS.template) != n_contrasts: - raise MRtrixError('mismatch between number of output templates (%i) ' % len(app.ARGS.template) + - 'and number of contrasts (%i)' % n_contrasts) - for templ in app.ARGS.template: - app.check_output_path(templ) + raise MRtrixError(f'mismatch between number of output templates ({len(app.ARGS.template)}) ' + 'and number of contrasts ({n_contrasts})') if app.ARGS.warp_dir: app.ARGS.warp_dir = relpath(app.ARGS.warp_dir) - app.check_output_path(app.ARGS.warp_dir) if app.ARGS.transformed_dir: - app.ARGS.transformed_dir = [relpath(d) for d in app.ARGS.transformed_dir.split(',')] + app.ARGS.transformed_dir = [app.Parser.DirectoryOut(d) for d in app.ARGS.transformed_dir.split(',')] if len(app.ARGS.transformed_dir) != n_contrasts: raise MRtrixError('require multiple comma separated transformed directories if multi-contrast registration is used') for tdir in app.ARGS.transformed_dir: - app.check_output_path(tdir) + tdir.check_output() if app.ARGS.linear_transformations_dir: if not dolinear: raise MRtrixError("linear option set when no linear registration is performed") app.ARGS.linear_transformations_dir = relpath(app.ARGS.linear_transformations_dir) - app.check_output_path(app.ARGS.linear_transformations_dir) # automatically detect SH series in each contrast do_fod_registration = False # in any contrast @@ -757,65 +938,69 @@ def execute(): #pylint: disable=unused-variable cns.fod_reorientation.append(False) cns.n_volumes.append(0) if do_fod_registration: - app.console("SH Series detected, performing FOD registration in contrast: " + - ', '.join(app.ARGS.input_dir[cid] for cid in range(n_contrasts) if cns.fod_reorientation[cid])) + fod_contrasts_dirs = [app.ARGS.input_dir[cid] for cid in range(n_contrasts) if cns.fod_reorientation[cid]] + app.console(f'SH Series detected, performing FOD registration in contrast: {", ".join(fod_contrasts_dirs)}') c_mrtransform_reorientation = [' -reorient_fod ' + ('yes' if cns.fod_reorientation[cid] else 'no') + ' ' for cid in range(n_contrasts)] if nanmask_input: - app.console("NaN masking transformed images") + app.console('NaN-masking transformed images') # rigid options if app.ARGS.rigid_scale: rigid_scales = app.ARGS.rigid_scale if not dorigid: - raise MRtrixError("rigid_scales option set when no rigid registration is performed") + raise MRtrixError('rigid_scales option set when no rigid registration is performed') else: rigid_scales = DEFAULT_RIGID_SCALES if app.ARGS.rigid_lmax: if not dorigid: - raise MRtrixError("rigid_lmax option set when no rigid registration is performed") + raise MRtrixError('-rigid_lmax option set when no rigid registration is performed') rigid_lmax = app.ARGS.rigid_lmax if do_fod_registration and len(rigid_scales) != len(rigid_lmax): - raise MRtrixError('rigid_scales and rigid_lmax schedules are not equal in length: scales stages: %s, lmax stages: %s' % (len(rigid_scales), len(rigid_lmax))) + raise MRtrixError(f'rigid_scales and rigid_lmax schedules are not equal in length: ' + f'scales stages: {len(rigid_scales)}, lmax stages: {len(rigid_lmax)}') else: rigid_lmax = DEFAULT_RIGID_LMAX rigid_niter = [100] * len(rigid_scales) if app.ARGS.rigid_niter: if not dorigid: - raise MRtrixError("rigid_niter specified when no rigid registration is performed") + raise MRtrixError('-rigid_niter specified when no rigid registration is performed') rigid_niter = app.ARGS.rigid_niter if len(rigid_niter) == 1: rigid_niter = rigid_niter * len(rigid_scales) elif len(rigid_scales) != len(rigid_niter): - raise MRtrixError('rigid_scales and rigid_niter schedules are not equal in length: scales stages: %s, niter stages: %s' % (len(rigid_scales), len(rigid_niter))) + raise MRtrixError(f'rigid_scales and rigid_niter schedules are not equal in length: ' + f'scales stages: {len(rigid_scales)}, niter stages: {len(rigid_niter)}') # affine options if app.ARGS.affine_scale: affine_scales = app.ARGS.affine_scale if not doaffine: - raise MRtrixError("affine_scale option set when no affine registration is performed") + raise MRtrixError('-affine_scale option set when no affine registration is performed') else: affine_scales = DEFAULT_AFFINE_SCALES if app.ARGS.affine_lmax: if not doaffine: - raise MRtrixError("affine_lmax option set when no affine registration is performed") + raise MRtrixError('affine_lmax option set when no affine registration is performed') affine_lmax = app.ARGS.affine_lmax if do_fod_registration and len(affine_scales) != len(affine_lmax): - raise MRtrixError('affine_scales and affine_lmax schedules are not equal in length: scales stages: %s, lmax stages: %s' % (len(affine_scales), len(affine_lmax))) + raise MRtrixError(f'affine_scales and affine_lmax schedules are not equal in length: ' + f'scales stages: {len(affine_scales)}, lmax stages: {len(affine_lmax)}') else: affine_lmax = DEFAULT_AFFINE_LMAX affine_niter = [500] * len(affine_scales) if app.ARGS.affine_niter: if not doaffine: - raise MRtrixError("affine_niter specified when no affine registration is performed") + raise MRtrixError('-affine_niter specified when no affine registration is performed') affine_niter = app.ARGS.affine_niter if len(affine_niter) == 1: affine_niter = affine_niter * len(affine_scales) elif len(affine_scales) != len(affine_niter): - raise MRtrixError('affine_scales and affine_niter schedules are not equal in length: scales stages: %s, niter stages: %s' % (len(affine_scales), len(affine_niter))) + raise MRtrixError(f'affine_scales and affine_niter schedules are not equal in length: ' + f'scales stages: {len(affine_scales)}, niter stages: {len(affine_niter)}') linear_scales = [] linear_lmax = [] @@ -839,17 +1024,17 @@ def execute(): #pylint: disable=unused-variable if len(linear_lmax) != len(linear_niter): mismatch = [] if len(rigid_lmax) != len(rigid_niter): - mismatch += ['rigid: lmax stages: %s, niter stages: %s' % (len(rigid_lmax), len(rigid_niter))] + mismatch += [f'rigid: lmax stages: {len(rigid_lmax)}, niter stages: {len(rigid_niter)}'] if len(affine_lmax) != len(affine_niter): - mismatch += ['affine: lmax stages: %s, niter stages: %s' % (len(affine_lmax), len(affine_niter))] - raise MRtrixError('linear registration: lmax and niter schedules are not equal in length: %s' % (', '.join(mismatch))) + mismatch += [f'affine: lmax stages: {len(affine_lmax)}, niter stages: {len(affine_niter)}'] + raise MRtrixError('linear registration: lmax and niter schedules are not equal in length: {", ".join(mismatch)}') app.console('-' * 60) - app.console('initial alignment of images: %s' % initial_alignment) + app.console(f'initial alignment of images: {initial_alignment}') app.console('-' * 60) if n_contrasts > 1: for cid in range(n_contrasts): - app.console('\tcontrast "%s": %s, ' % (cns.suff[cid], cns.names[cid]) + - 'objective weight: %s' % ','.join(str(item) for item in cns.mc_weight_initial_alignment[cid])) + app.console(f'\tcontrast "{cns.suff[cid]}": {cns.names[cid]}, ' + f'objective weight: {",".join(map(str, cns.mc_weight_initial_alignment[cid]))}') if dolinear: app.console('-' * 60) @@ -857,19 +1042,19 @@ def execute(): #pylint: disable=unused-variable app.console('-' * 60) if n_contrasts > 1: for cid in range(n_contrasts): - msg = '\tcontrast "%s": %s' % (cns.suff[cid], cns.names[cid]) + msg = f'\tcontrast "{cns.suff[cid]}": {cns.names[cid]}' if 'rigid' in linear_type: - msg += ', objective weight rigid: %s' % ','.join(str(item) for item in cns.mc_weight_rigid[cid]) + msg += f', objective weight rigid: {",".join(map(str, cns.mc_weight_rigid[cid]))}' if 'affine' in linear_type: - msg += ', objective weight affine: %s' % ','.join(str(item) for item in cns.mc_weight_affine[cid]) + msg += f', objective weight affine: {",".join(map(str, cns.mc_weight_affine[cid]))}' app.console(msg) if do_fod_registration: for istage, [tpe, scale, lmax, niter] in enumerate(zip(linear_type, linear_scales, linear_lmax, linear_niter)): - app.console('(%02i) %s scale: %.4f, niter: %i, lmax: %i' % (istage, tpe.ljust(9), scale, niter, lmax)) + app.console(f'({istage:02d}) {tpe.ljust(9)} scale: {scale:.4f}, niter: {niter}, lmax: {lmax}') else: for istage, [tpe, scale, niter] in enumerate(zip(linear_type, linear_scales, linear_niter)): - app.console('(%02i) %s scale: %.4f, niter: %i, no reorientation' % (istage, tpe.ljust(9), scale, niter)) + app.console(f'({istage:02d}) {tpe.ljust(9)} scale: {scale:.4f}, niter: {niter}, no reorientation') datatype_option = ' -datatype float32' outofbounds_option = ' -nan' @@ -879,54 +1064,56 @@ def execute(): #pylint: disable=unused-variable nl_lmax = [] nl_niter = [] if app.ARGS.warp_dir: - raise MRtrixError('warp_dir specified when no nonlinear registration is performed') + raise MRtrixError('-warp_dir specified when no nonlinear registration is performed') else: nl_scales = app.ARGS.nl_scale if app.ARGS.nl_scale else DEFAULT_NL_SCALES nl_niter = app.ARGS.nl_niter if app.ARGS.nl_niter else DEFAULT_NL_NITER nl_lmax = app.ARGS.nl_lmax if app.ARGS.nl_lmax else DEFAULT_NL_LMAX if len(nl_scales) != len(nl_niter): - raise MRtrixError('nl_scales and nl_niter schedules are not equal in length: scales stages: %s, niter stages: %s' % (len(nl_scales), len(nl_niter))) + raise MRtrixError(f'nl_scales and nl_niter schedules are not equal in length: ' + f'scales stages: {len(nl_scales)}, niter stages: {len(nl_niter)}') app.console('-' * 60) app.console('nonlinear registration stages:') app.console('-' * 60) if n_contrasts > 1: for cid in range(n_contrasts): - app.console('\tcontrast "%s": %s, objective weight: %s' % (cns.suff[cid], cns.names[cid], ','.join(str(item) for item in cns.mc_weight_nl[cid]))) + app.console(f'\tcontrast "{cns.suff[cid]}": {cns.names[cid]}, ' + f'objective weight: {",".join(map(str, cns.mc_weight_nl[cid]))}') if do_fod_registration: if len(nl_scales) != len(nl_lmax): - raise MRtrixError('nl_scales and nl_lmax schedules are not equal in length: scales stages: %s, lmax stages: %s' % (len(nl_scales), len(nl_lmax))) + raise MRtrixError(f'nl_scales and nl_lmax schedules are not equal in length: ' + f'scales stages: {len(nl_scales)}, lmax stages: {len(nl_lmax)}') if do_fod_registration: for istage, [scale, lmax, niter] in enumerate(zip(nl_scales, nl_lmax, nl_niter)): - app.console('(%02i) nonlinear scale: %.4f, niter: %i, lmax: %i' % (istage, scale, niter, lmax)) + app.console(f'({istage:02d}) nonlinear scale: {scale:.4f}, niter: {niter}, lmax: {lmax}') else: for istage, [scale, niter] in enumerate(zip(nl_scales, nl_niter)): - app.console('(%02i) nonlinear scale: %.4f, niter: %i, no reorientation' % (istage, scale, niter)) + app.console('({istage:02d}) nonlinear scale: {scale:.4f}, niter: {niter}, no reorientation') app.console('-' * 60) app.console('input images:') app.console('-' * 60) for inp in ins: - app.console('\t' + inp.info()) + app.console(f'\t{inp.info()}') - app.make_scratch_dir() - app.goto_scratch_dir() + app.activate_scratch_dir() for contrast in cns.suff: - path.make_dir('input_transformed' + contrast) + path.make_dir(f'input_transformed{contrast}') for contrast in cns.suff: - path.make_dir('isfinite' + contrast) + path.make_dir(f'isfinite{contrast}') path.make_dir('linear_transforms_initial') path.make_dir('linear_transforms') for level in range(0, len(linear_scales)): - path.make_dir('linear_transforms_%02i' % level) + path.make_dir(f'linear_transforms_{level:02d}') for level in range(0, len(nl_scales)): - path.make_dir('warps_%02i' % level) + path.make_dir(f'warps_{level:02d}') if use_masks: path.make_dir('mask_transformed') @@ -957,7 +1144,7 @@ def execute(): #pylint: disable=unused-variable if use_masks: progress = app.ProgressBar('Importing input masks to average space for template cropping', len(ins)) for inp in ins: - run.command('mrtransform ' + inp.msk_path + ' -interp nearest -template average_header.mif ' + inp.msk_transformed) + run.command(f'mrtransform {inp.msk_path} -interp nearest -template average_header.mif {inp.msk_transformed}') progress.increment() progress.done() run.command(['mrmath', [inp.msk_transformed for inp in ins], 'max', 'mask_initial.mif']) @@ -978,39 +1165,36 @@ def execute(): #pylint: disable=unused-variable if len(image.Header('average_header.mif').size()) == 3: run.command('mrconvert average_header.mif ' + avh3d) else: - run.command('mrconvert average_header.mif -coord 3 0 -axes 0,1,2 ' + avh3d) - run.command('mrconvert ' + avh3d + ' -axes 0,1,2,-1 ' + avh4d) + run.command(f'mrconvert average_header.mif -coord 3 0 -axes 0,1,2 {avh3d}') + run.command(f'mrconvert {avh3d} -axes 0,1,2,-1 {avh4d}') for cid in range(n_contrasts): if cns.n_volumes[cid] == 0: - run.function(copy, avh3d, 'average_header' + cns.suff[cid] + '.mif') + run.function(copy, avh3d, f'average_header{cns.suff[cid]}.mif') elif cns.n_volumes[cid] == 1: - run.function(copy, avh4d, 'average_header' + cns.suff[cid] + '.mif') + run.function(copy, avh4d, f'average_header{cns.suff[cid]}.mif') else: - run.command(['mrcat', [avh3d] * cns.n_volumes[cid], '-axis', '3', 'average_header' + cns.suff[cid] + '.mif']) + run.command(['mrcat', [avh3d] * cns.n_volumes[cid], '-axis', '3', f'average_header{cns.suff[cid]}.mif']) run.function(os.remove, avh3d) run.function(os.remove, avh4d) else: - run.function(shutil.move, 'average_header.mif', 'average_header' + cns.suff[0] + '.mif') + run.function(shutil.move, 'average_header.mif', f'average_header{cns.suff[0]}.mif') - cns.templates = ['average_header' + csuff + '.mif' for csuff in cns.suff] + cns.templates = [f'average_header{csuff}.mif' for csuff in cns.suff] if initial_alignment == 'none': progress = app.ProgressBar('Resampling input images to template space with no initial alignment', len(ins) * n_contrasts) for inp in ins: for cid in range(n_contrasts): - run.command('mrtransform ' + inp.ims_path[cid] + c_mrtransform_reorientation[cid] + ' -interp linear ' + - '-template ' + cns.templates[cid] + ' ' + inp.ims_transformed[cid] + - outofbounds_option + - datatype_option) + run.command(f'mrtransform {inp.ims_path[cid]} {c_mrtransform_reorientation[cid]} -interp linear '\ + f'-template {cns.templates[cid]} {inp.ims_transformed[cid]} {outofbounds_option} {datatype_option}') progress.increment() progress.done() if use_masks: progress = app.ProgressBar('Reslicing input masks to average header', len(ins)) for inp in ins: - run.command('mrtransform ' + inp.msk_path + ' ' + inp.msk_transformed + ' ' + - '-interp nearest -template ' + cns.templates[0] + ' ' + - datatype_option) + run.command(f'mrtransform {inp.msk_path} {inp.msk_transformed} ' + f'-interp nearest -template {cns.templates[0]} {datatype_option}') progress.increment() progress.done() @@ -1023,10 +1207,10 @@ def execute(): #pylint: disable=unused-variable if not dolinear: for inp in ins: - with open(os.path.join('linear_transforms_initial', inp.uid + '.txt'), 'w', encoding='utf-8') as fout: + with open(os.path.join('linear_transforms_initial', f'{inp.uid}.txt'), 'w', encoding='utf-8') as fout: fout.write('1 0 0 0\n0 1 0 0\n0 0 1 0\n0 0 0 1\n') - run.function(copy, 'average_header' + cns.suff[0] + '.mif', 'average_header.mif') + run.function(copy, f'average_header{cns.suff[0]}.mif', 'average_header.mif') else: progress = app.ProgressBar('Performing initial rigid registration to template', len(ins)) @@ -1035,29 +1219,30 @@ def execute(): #pylint: disable=unused-variable lmax_option = ' -rigid_lmax 0 ' if cns.fod_reorientation[cid] else ' -noreorientation ' contrast_weight_option = cns.initial_alignment_weight_option for inp in ins: - output_option = ' -rigid ' + os.path.join('linear_transforms_initial', inp.uid + '.txt') - images = ' '.join([p + ' ' + t for p, t in zip(inp.ims_path, cns.templates)]) + output_option = ' -rigid ' + os.path.join('linear_transforms_initial', f'{inp.uid}.txt') + images = ' '.join([f'{p} {t}' for p, t in zip(inp.ims_path, cns.templates)]) if use_masks: - mask_option = ' -mask1 ' + inp.msk_path + mask_option = f' -mask1 {inp.msk_path}' if initial_alignment == 'robust_mass': if not os.path.isfile('robust/template.mif'): if cns.n_volumes[cid] > 0: - run.command('mrconvert ' + cns.templates[cid] + ' -coord 3 0 - | mrconvert - -axes 0,1,2 robust/template.mif') + run.command(f'mrconvert {cns.templates[cid]} -coord 3 0 - | ' + f'mrconvert - -axes 0,1,2 robust/template.mif') else: - run.command('mrconvert ' + cns.templates[cid] + ' robust/template.mif') + run.command(f'mrconvert {cns.templates[cid]} robust/template.mif') if n_contrasts > 1: - cmd = ['mrcalc', inp.ims_path[cid], ','.join(str(item) for item in cns.mc_weight_initial_alignment[cid]), '-mult'] + cmd = ['mrcalc', inp.ims_path[cid], ','.join(map(str, cns.mc_weight_initial_alignment[cid])), '-mult'] for cid in range(1, n_contrasts): - cmd += [inp.ims_path[cid], ','.join(str(item) for item in cns.mc_weight_initial_alignment[cid]), '-mult', '-add'] + cmd += [inp.ims_path[cid], ','.join(map(str, cns.mc_weight_initial_alignment[cid])), '-mult', '-add'] contrast_weight_option = '' - run.command(' '.join(cmd) + - ' - | mrfilter - zclean -zlower 3 -zupper 3 robust/image_' + inp.uid + '.mif' - ' -maskin ' + inp.msk_path + ' -maskout robust/mask_' + inp.uid + '.mif') + run.command(' '.join(cmd) + ' - | ' \ + f'mrfilter - zclean -zlower 3 -zupper 3 robust/image_{inp.uid}.mif ' + f'-maskin {inp.msk_path} -maskout robust/mask_{inp.uid}.mif') else: - run.command('mrfilter ' + inp.ims_path[0] + ' zclean -zlower 3 -zupper 3 robust/image_' + inp.uid + '.mif' + - ' -maskin ' + inp.msk_path + ' -maskout robust/mask_' + inp.uid + '.mif') - images = 'robust/image_' + inp.uid + '.mif robust/template.mif' - mask_option = ' -mask1 ' + 'robust/mask_' + inp.uid + '.mif' + run.command(f'mrfilter {inp.ims_path[0]} zclean -zlower 3 -zupper 3 robust/image_{inp.uid}.mif ' + f'-maskin {inp.msk_path} -maskout robust/mask_{inp.uid}.mif') + images = f'robust/image_{inp.uid}.mif robust/template.mif' + mask_option = f' -mask1 robust/mask_{inp.uid}.mif' lmax_option = '' run.command('mrregister ' + images + @@ -1071,18 +1256,18 @@ def execute(): #pylint: disable=unused-variable datatype_option + output_option) # translate input images to centre of mass without interpolation + transform_path = os.path.join('linear_transforms_initial', f'{inp.uid}.txt') for cid in range(n_contrasts): - run.command('mrtransform ' + inp.ims_path[cid] + c_mrtransform_reorientation[cid] + - ' -linear ' + os.path.join('linear_transforms_initial', inp.uid + '.txt') + - ' ' + inp.ims_transformed[cid] + "_translated.mif" + datatype_option) + run.command(f'mrtransform {inp.ims_path[cid]} {c_mrtransform_reorientation[cid]} ' + f'-linear {transform_path} ' + f'{inp.ims_transformed[cid]}_translated.mif {datatype_option}') if use_masks: - run.command('mrtransform ' + inp.msk_path + - ' -linear ' + os.path.join('linear_transforms_initial', inp.uid + '.txt') + - ' ' + inp.msk_transformed + "_translated.mif" + - datatype_option) + run.command(f'mrtransform {inp.msk_path} ' + f'-linear {transform_path} ' + f'{inp.msk_transformed}_translated.mif {datatype_option}') progress.increment() # update average space of first contrast to new extent, delete other average space images - run.command(['mraverageheader', [inp.ims_transformed[cid] + '_translated.mif' for inp in ins], 'average_header_tight.mif']) + run.command(['mraverageheader', [f'{inp.ims_transformed[cid]}_translated.mif' for inp in ins], 'average_header_tight.mif']) progress.done() if voxel_size is None: @@ -1092,14 +1277,14 @@ def execute(): #pylint: disable=unused-variable 'mrgrid - regrid -voxel ' + ','.join(map(str, voxel_size)) + ' average_header.mif', force=True) run.function(os.remove, 'average_header_tight.mif') for cid in range(1, n_contrasts): - run.function(os.remove, 'average_header' + cns.suff[cid] + '.mif') + run.function(os.remove, f'average_header{cns.suff[cid]}.mif') if use_masks: # reslice masks progress = app.ProgressBar('Reslicing input masks to average header', len(ins)) for inp in ins: - run.command('mrtransform ' + inp.msk_transformed + '_translated.mif' + ' ' + inp.msk_transformed + ' ' + - '-interp nearest -template average_header.mif' + datatype_option) + run.command(f'mrtransform {inp.msk_transformed}_translated.mif {inp.msk_transformed} ' + f'-interp nearest -template average_header.mif {datatype_option}') progress.increment() progress.done() # crop average space to extent defined by translated masks @@ -1111,9 +1296,10 @@ def execute(): #pylint: disable=unused-variable # reslice masks progress = app.ProgressBar('Reslicing masks to new padded average header', len(ins)) for inp in ins: - run.command('mrtransform ' + inp.msk_transformed + '_translated.mif ' + inp.msk_transformed + ' ' + - '-interp nearest -template average_header.mif' + datatype_option, force=True) - run.function(os.remove, inp.msk_transformed + '_translated.mif') + run.command(f'mrtransform {inp.msk_transformed}_translated.mif {inp.msk_transformed} ' + f'-interp nearest -template average_header.mif {datatype_option}', + force=True) + run.function(os.remove, f'{inp.msk_transformed}_translated.mif') progress.increment() progress.done() run.function(os.remove, 'mask_translated.mif') @@ -1122,12 +1308,11 @@ def execute(): #pylint: disable=unused-variable progress = app.ProgressBar('Reslicing input images to average header', len(ins) * n_contrasts) for cid in range(n_contrasts): for inp in ins: - run.command('mrtransform ' + c_mrtransform_reorientation[cid] + inp.ims_transformed[cid] + '_translated.mif ' + - inp.ims_transformed[cid] + ' ' + - ' -interp linear -template average_header.mif' + - outofbounds_option + - datatype_option) - run.function(os.remove, inp.ims_transformed[cid] + '_translated.mif') + run.command(f'mrtransform {c_mrtransform_reorientation[cid]} {inp.ims_transformed[cid]}_translated.mif ' + f'{inp.ims_transformed[cid]} ' + f'-interp linear -template average_header.mif ' + f'{outofbounds_option} {datatype_option}') + run.function(os.remove, f'{inp.ims_transformed[cid]}_translated.mif') progress.increment() progress.done() @@ -1138,73 +1323,77 @@ def execute(): #pylint: disable=unused-variable if leave_one_out: calculate_isfinite(ins, cns) - cns.templates = ['initial_template' + contrast + '.mif' for contrast in cns.suff] + cns.templates = [f'initial_template{contrast}.mif' for contrast in cns.suff] for cid in range(n_contrasts): - aggregate(ins, 'initial_template' + cns.suff[cid] + '.mif', cid, agg_measure) + aggregate(ins, f'initial_template{cns.suff[cid]}.mif', cid, agg_measure) if cns.n_volumes[cid] == 1: - run.function(shutil.move, 'initial_template' + cns.suff[cid] + '.mif', 'tmp.mif') - run.command('mrconvert tmp.mif initial_template' + cns.suff[cid] + '.mif -axes 0,1,2,-1') + run.function(shutil.move, f'initial_template{cns.suff[cid]}.mif', 'tmp.mif') + run.command(f'mrconvert tmp.mif initial_template{cns.suff[cid]}.mif -axes 0,1,2,-1') # Optimise template with linear registration if not dolinear: for inp in ins: - run.function(copy, os.path.join('linear_transforms_initial', inp.uid+'.txt'), - os.path.join('linear_transforms', inp.uid+'.txt')) + run.function(copy, os.path.join('linear_transforms_initial', f'{inp.uid}.txt'), + os.path.join('linear_transforms', f'{inp.uid}.txt')) else: level = 0 regtype = linear_type[0] def linear_msg(): - return 'Optimising template with linear registration (stage {0} of {1}; {2})'.format(level + 1, len(linear_scales), regtype) + return f'Optimising template with linear registration (stage {level+1} of {len(linear_scales)}; {regtype})' progress = app.ProgressBar(linear_msg, len(linear_scales) * len(ins) * (1 + n_contrasts + int(use_masks))) for level, (regtype, scale, niter, lmax) in enumerate(zip(linear_type, linear_scales, linear_niter, linear_lmax)): for inp in ins: initialise_option = '' if use_masks: - mask_option = ' -mask1 ' + inp.msk_path + mask_option = f' -mask1 {inp.msk_path}' else: mask_option = '' lmax_option = ' -noreorientation' metric_option = '' mrregister_log_option = '' + output_transform_path = os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') + init_transform_path = os.path.join(f'linear_transforms_{level-1:02d}' if level > 0 else 'linear_transforms_initial', + f'{inp.uid}.txt') + linear_log_path = os.path.join('log', f'{inp.uid}{contrast[cid]}_{level}.log') if regtype == 'rigid': - scale_option = ' -rigid_scale ' + str(scale) - niter_option = ' -rigid_niter ' + str(niter) + scale_option = f' -rigid_scale {scale}' + niter_option = f' -rigid_niter {niter}' regtype_option = ' -type rigid' - output_option = ' -rigid ' + os.path.join('linear_transforms_%02i' % level, inp.uid + '.txt') + output_option = f' -rigid {output_transform_path}' contrast_weight_option = cns.rigid_weight_option - initialise_option = (' -rigid_init_matrix ' + - os.path.join('linear_transforms_%02i' % (level - 1) if level > 0 else 'linear_transforms_initial', inp.uid + '.txt')) + + initialise_option = f' -rigid_init_matrix {init_transform_path}' if do_fod_registration: - lmax_option = ' -rigid_lmax ' + str(lmax) + lmax_option = f' -rigid_lmax {lmax}' if linear_estimator is not None: - metric_option = ' -rigid_metric.diff.estimator ' + linear_estimator + metric_option = f' -rigid_metric.diff.estimator {linear_estimator}' if app.VERBOSITY >= 2: - mrregister_log_option = ' -info -rigid_log ' + os.path.join('log', inp.uid + contrast[cid] + "_" + str(level) + '.log') + mrregister_log_option = f' -info -rigid_log {linear_log_path}' else: - scale_option = ' -affine_scale ' + str(scale) - niter_option = ' -affine_niter ' + str(niter) + scale_option = f' -affine_scale {scale}' + niter_option = f' -affine_niter {niter}' regtype_option = ' -type affine' - output_option = ' -affine ' + os.path.join('linear_transforms_%02i' % level, inp.uid + '.txt') + output_option = ' -affine {output_transform_path}' contrast_weight_option = cns.affine_weight_option - initialise_option = (' -affine_init_matrix ' + - os.path.join('linear_transforms_%02i' % (level - 1) if level > 0 else 'linear_transforms_initial', inp.uid + '.txt')) + initialise_option = ' -affine_init_matrix {init_transform_path}' if do_fod_registration: - lmax_option = ' -affine_lmax ' + str(lmax) + lmax_option = f' -affine_lmax {lmax}' if linear_estimator is not None: - metric_option = ' -affine_metric.diff.estimator ' + linear_estimator + metric_option = f' -affine_metric.diff.estimator {linear_estimator}' if write_log: - mrregister_log_option = ' -info -affine_log ' + os.path.join('log', inp.uid + contrast[cid] + "_" + str(level) + '.log') + mrregister_log_option = f' -info -affine_log {linear_log_path}' if leave_one_out: tmpl = [] for cid in range(n_contrasts): - isfinite = 'isfinite%s/%s.mif' % (cns.suff[cid], inp.uid) + isfinite = f'isfinite{cns.suff[cid]}/{inp.uid}.mif' weight = inp.aggregation_weight if inp.aggregation_weight is not None else '1' # loo = (template * weighted sum - weight * this) / (weighted sum - weight) - run.command('mrcalc ' + cns.isfinite_count[cid] + ' ' + isfinite + ' -sub - | mrcalc ' + cns.templates[cid] + - ' ' + cns.isfinite_count[cid] + ' -mult ' + inp.ims_transformed[cid] + ' ' + weight + ' -mult ' + - ' -sub - -div loo_%s' % cns.templates[cid], force=True) - tmpl.append('loo_%s' % cns.templates[cid]) + run.command(f'mrcalc {cns.isfinite_count[cid]} {isfinite} -sub - | ' + f'mrcalc {cns.templates[cid]} {cns.isfinite_count[cid]} -mult {inp.ims_transformed[cid]} {weight} -mult -sub - -div ' + f'loo_{cns.templates[cid]}', + force=True) + tmpl.append(f'loo_{cns.templates[cid]}') images = ' '.join([p + ' ' + t for p, t in zip(inp.ims_path, tmpl)]) else: images = ' '.join([p + ' ' + t for p, t in zip(inp.ims_path, cns.templates)]) @@ -1221,7 +1410,8 @@ def execute(): #pylint: disable=unused-variable output_option + \ mrregister_log_option run.command(command, force=True) - check_linear_transformation(os.path.join('linear_transforms_%02i' % level, inp.uid + '.txt'), command, + check_linear_transformation(os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt'), + command, pause_on_warn=do_pause_on_warn) if leave_one_out: for im_temp in tmpl: @@ -1242,53 +1432,64 @@ def execute(): #pylint: disable=unused-variable # - If one subject's registration fails, this will affect the average and therefore the template which could result in instable behaviour. # - The template appearance changes slightly over levels, but the template and trafos are affected in the same way so should not affect template convergence. if not app.ARGS.linear_no_drift_correction: - run.command(['transformcalc', [os.path.join('linear_transforms_initial', inp.uid + '.txt') for _inp in ins], + # TODO Is this a bug? + # Seems to be averaging N instances of the last input, rather than averaging all inputs + # TODO Further, believe that the first transformcalc call only needs to be performed once; + # should not need to be recomputed between iterations + run.command(['transformcalc', [os.path.join('linear_transforms_initial', f'{inp.uid}.txt') for _inp in ins], 'average', 'linear_transform_average_init.txt', '-quiet'], force=True) - run.command(['transformcalc', [os.path.join('linear_transforms_%02i' % level, inp.uid + '.txt') for _inp in ins], - 'average', 'linear_transform_average_%02i_uncorrected.txt' % level, '-quiet'], force=True) - run.command(['transformcalc', 'linear_transform_average_%02i_uncorrected.txt' % level, - 'invert', 'linear_transform_average_%02i_uncorrected_inv.txt' % level, '-quiet'], force=True) + run.command(['transformcalc', [os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') for _inp in ins], + 'average', f'linear_transform_average_{level:02d}_uncorrected.txt', '-quiet'], force=True) + run.command(['transformcalc', 'linear_transform_average_{level:02d}_uncorrected.txt', + 'invert', f'linear_transform_average_{level:02d}_uncorrected_inv.txt', '-quiet'], force=True) transform_average_init = matrix.load_transform('linear_transform_average_init.txt') - transform_average_current_inv = matrix.load_transform('linear_transform_average_%02i_uncorrected_inv.txt' % level) + transform_average_current_inv = matrix.load_transform(f'linear_transform_average_{level:02d}_uncorrected_inv.txt') transform_update = matrix.dot(transform_average_init, transform_average_current_inv) - matrix.save_transform(os.path.join('linear_transforms_%02i_drift_correction.txt' % level), transform_update, force=True) + matrix.save_transform(f'linear_transforms_{level:02d}_drift_correction.txt', transform_update, force=True) if regtype == 'rigid': - run.command('transformcalc ' + os.path.join('linear_transforms_%02i_drift_correction.txt' % level) + - ' rigid ' + os.path.join('linear_transforms_%02i_drift_correction.txt' % level) + ' -quiet', force=True) - transform_update = matrix.load_transform(os.path.join('linear_transforms_%02i_drift_correction.txt' % level)) + run.command(['transformcalc', + f'linear_transforms_{level:02d}_drift_correction.txt', + 'rigid', + f'linear_transforms_{level:02d}_drift_correction.txt', + '-quiet'], + force=True) + transform_update = matrix.load_transform(f'linear_transforms_{level:02d}_drift_correction.txt') for inp in ins: - transform = matrix.load_transform('linear_transforms_%02i/' % level + inp.uid + '.txt') + transform = matrix.load_transform(os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt')) transform_updated = matrix.dot(transform, transform_update) - run.function(copy, 'linear_transforms_%02i/' % level + inp.uid + '.txt', 'linear_transforms_%02i/' % level + inp.uid + '.precorrection') - matrix.save_transform(os.path.join('linear_transforms_%02i' % level, inp.uid + '.txt'), transform_updated, force=True) + run.function(copy, + os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt'), + os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.precorrection')) + matrix.save_transform(os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt'), transform_updated, force=True) # compute average trafos and its properties for easier debugging - run.command(['transformcalc', [os.path.join('linear_transforms_%02i' % level, _inp.uid + '.txt') for _inp in ins], - 'average', 'linear_transform_average_%02i.txt' % level, '-quiet'], force=True) - run.command('transformcalc linear_transform_average_%02i.txt decompose linear_transform_average_%02i.dec' % (level, level), force=True) + run.command(['transformcalc', [os.path.join(f'linear_transforms_{level:02d}', f'{_inp.uid}.txt') for _inp in ins], + 'average', f'linear_transform_average_{level:02d}.txt', '-quiet'], force=True) + run.command(f'transformcalc linear_transform_average_{level:02d}.txt decompose linear_transform_average_{level:02d}.dec', + force=True) for cid in range(n_contrasts): for inp in ins: - run.command('mrtransform ' + c_mrtransform_reorientation[cid] + inp.ims_path[cid] + - ' -template ' + cns.templates[cid] + - ' -linear ' + os.path.join('linear_transforms_%02i' % level, inp.uid + '.txt') + - ' ' + inp.ims_transformed[cid] + - outofbounds_option + - datatype_option, + transform_path = os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') + run.command(f'mrtransform {c_mrtransform_reorientation[cid]} {inp.ims_path[cid]}' + f' -template {cns.templates[cid]} ' + f' -linear {transform_path}' + f' {inp.ims_transformed[cid]} {outofbounds_option} {datatype_option}', force=True) progress.increment() if use_masks: for inp in ins: - run.command('mrtransform ' + inp.msk_path + - ' -template ' + cns.templates[0] + - ' -interp nearest' + - ' -linear ' + os.path.join('linear_transforms_%02i' % level, inp.uid + '.txt') + - ' ' + inp.msk_transformed, + transform_path = os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') + run.command(f'mrtransform {inp.msk_path} ' + f' -template {cns.templates[0]} ' + f' -interp nearest' + f' -linear {transform_path}' + f' {inp.msk_transformed}', force=True) progress.increment() @@ -1302,48 +1503,54 @@ def execute(): #pylint: disable=unused-variable for cid in range(n_contrasts): if level > 0 and app.ARGS.delete_temporary_files: os.remove(cns.templates[cid]) - cns.templates[cid] = 'linear_template%02i%s.mif' % (level, cns.suff[cid]) + cns.templates[cid] = f'linear_template{level:02d}{cns.suff[cid]}.mif' aggregate(ins, cns.templates[cid], cid, agg_measure) if cns.n_volumes[cid] == 1: run.function(shutil.move, cns.templates[cid], 'tmp.mif') - run.command('mrconvert tmp.mif ' + cns.templates[cid] + ' -axes 0,1,2,-1') + run.command(f'mrconvert tmp.mif {cns.templates[cid]} -axes 0,1,2,-1') run.function(os.remove, 'tmp.mif') - for entry in os.listdir('linear_transforms_%02i' % level): - run.function(copy, os.path.join('linear_transforms_%02i' % level, entry), os.path.join('linear_transforms', entry)) + for entry in os.listdir(f'linear_transforms_{level:02d}'): + run.function(copy, + os.path.join(f'linear_transforms_{level:02d}', entry), + os.path.join('linear_transforms', entry)) progress.done() # Create a template mask for nl registration by taking the intersection of all transformed input masks and dilating if use_masks and (dononlinear or app.ARGS.template_mask): - run.command(['mrmath', path.all_in_dir('mask_transformed')] + - 'min - | maskfilter - median - | maskfilter - dilate -npass 5 init_nl_template_mask.mif'.split(), force=True) current_template_mask = 'init_nl_template_mask.mif' + run.command(['mrmath', path.all_in_dir('mask_transformed'), 'min', '-', '|', + 'maskfilter', '-', 'median', '-', '|', + 'maskfilter', '-', 'dilate', '-npass', '5', current_template_mask], + force=True) if dononlinear: path.make_dir('warps') level = 0 def nonlinear_msg(): - return 'Optimising template with non-linear registration (stage {0} of {1})'.format(level + 1, len(nl_scales)) + return f'Optimising template with non-linear registration (stage {level+1} of {len(nl_scales)})' progress = app.ProgressBar(nonlinear_msg, len(nl_scales) * len(ins)) for level, (scale, niter, lmax) in enumerate(zip(nl_scales, nl_niter, nl_lmax)): for inp in ins: if level > 0: - initialise_option = ' -nl_init ' + os.path.join('warps_%02i' % (level - 1), inp.uid + '.mif') + init_warp_path = os.path.join(f'warps_{level-1:02d}', f'{inp.uid}.mif') + initialise_option = f' -nl_init {init_warp_path}' scale_option = '' else: - scale_option = ' -nl_scale ' + str(scale) + scale_option = f' -nl_scale {scale}' + init_transform_path = os.path.join('linear_transforms', f'{inp.uid}.txt') if not doaffine: # rigid or no previous linear stage - initialise_option = ' -rigid_init_matrix ' + os.path.join('linear_transforms', inp.uid + '.txt') + initialise_option = f' -rigid_init_matrix {init_transform_path}' else: - initialise_option = ' -affine_init_matrix ' + os.path.join('linear_transforms', inp.uid + '.txt') + initialise_option = f' -affine_init_matrix {init_transform_path}' if use_masks: - mask_option = ' -mask1 ' + inp.msk_path + ' -mask2 ' + current_template_mask + mask_option = f' -mask1 {inp.msk_path} -mask2 {current_template_mask}' else: mask_option = '' if do_fod_registration: - lmax_option = ' -nl_lmax ' + str(lmax) + lmax_option = f' -nl_lmax {lmax}' else: lmax_option = ' -noreorientation' @@ -1352,40 +1559,36 @@ def execute(): #pylint: disable=unused-variable if leave_one_out: tmpl = [] for cid in range(n_contrasts): - isfinite = 'isfinite%s/%s.mif' % (cns.suff[cid], inp.uid) + isfinite = f'isfinite{cns.suff[cid]}/{inp.uid}.mif' weight = inp.aggregation_weight if inp.aggregation_weight is not None else '1' # loo = (template * weighted sum - weight * this) / (weighted sum - weight) - run.command('mrcalc ' + cns.isfinite_count[cid] + ' ' + isfinite + ' -sub - | mrcalc ' + cns.templates[cid] + - ' ' + cns.isfinite_count[cid] + ' -mult ' + inp.ims_transformed[cid] + ' ' + weight + ' -mult ' + - ' -sub - -div loo_%s' % cns.templates[cid], force=True) - tmpl.append('loo_%s' % cns.templates[cid]) - images = ' '.join([p + ' ' + t for p, t in zip(inp.ims_path, tmpl)]) + run.command(f'mrcalc {cns.isfinite_count[cid]} {isfinite} -sub - | ' + f'mrcalc {cns.templates[cid]} {cns.isfinite_count[cid]} -mult {inp.ims_transformed[cid]} {weight} -mult -sub - -div ' + f'loo_{cns.templates[cid]}', + force=True) + tmpl.append(f'loo_{cns.templates[cid]}') + images = ' '.join([f'{p} {t}' for p, t in zip(inp.ims_path, tmpl)]) else: - images = ' '.join([p + ' ' + t for p, t in zip(inp.ims_path, cns.templates)]) - run.command('mrregister ' + images + - ' -type nonlinear' + - ' -nl_niter ' + str(nl_niter[level]) + - ' -nl_warp_full ' + os.path.join('warps_%02i' % level, inp.uid + '.mif') + - ' -transformed ' + - ' -transformed '.join([inp.ims_transformed[cid] for cid in range(n_contrasts)]) + ' ' + - ' -nl_update_smooth ' + str(app.ARGS.nl_update_smooth) + - ' -nl_disp_smooth ' + str(app.ARGS.nl_disp_smooth) + - ' -nl_grad_step ' + str(app.ARGS.nl_grad_step) + - initialise_option + - contrast_weight_option + - scale_option + - mask_option + - datatype_option + - outofbounds_option + - lmax_option, + images = ' '.join([f'{p} {t}' for p, t in zip(inp.ims_path, cns.templates)]) + warp_full_path = os.path.join(f'warps_{level:02d}', f'{inp.uid}.mif') + run.command(f'mrregister {images}' + f' -type nonlinear' + f' -nl_niter {nl_niter[level]}' + f' -nl_warp_full {warp_full_path}' + f' -transformed {" -transformed ".join([inp.ims_transformed[cid] for cid in range(n_contrasts)])}' + f' -nl_update_smooth {app.ARGS.nl_update_smooth}' + f' -nl_disp_smooth {app.ARGS.nl_disp_smooth}' + f' -nl_grad_step {app.ARGS.nl_grad_step} ' + f' {initialise_option} {contrast_weight_option} {scale_option} {mask_option} {datatype_option} {outofbounds_option} {lmax_option}', force=True) if use_masks: - run.command('mrtransform ' + inp.msk_path + - ' -template ' + cns.templates[0] + - ' -warp_full ' + os.path.join('warps_%02i' % level, inp.uid + '.mif') + - ' ' + inp.msk_transformed + - ' -interp nearest ', + warp_full_path = os.path.join(f'warps_{level:02d}', f'{inp.uid}.mif') + run.command(f'mrtransform {inp.msk_path}' + f' -template {cns.templates[0]}' + f' -warp_full {warp_full_path}' + f' {inp.msk_transformed}' + f' -interp nearest', force=True) if leave_one_out: @@ -1393,7 +1596,7 @@ def execute(): #pylint: disable=unused-variable run.function(os.remove, im_temp) if level > 0: - run.function(os.remove, os.path.join('warps_%02i' % (level - 1), inp.uid + '.mif')) + run.function(os.remove, os.path.join(f'warps_{level-1:02d}', f'{inp.uid}.mif')) progress.increment(nonlinear_msg()) @@ -1407,83 +1610,84 @@ def execute(): #pylint: disable=unused-variable for cid in range(n_contrasts): if level > 0 and app.ARGS.delete_temporary_files: os.remove(cns.templates[cid]) - cns.templates[cid] = 'nl_template%02i%s.mif' % (level, cns.suff[cid]) + cns.templates[cid] = f'nl_template{level:02d}{cns.suff[cid]}.mif' aggregate(ins, cns.templates[cid], cid, agg_measure) if cns.n_volumes[cid] == 1: run.function(shutil.move, cns.templates[cid], 'tmp.mif') - run.command('mrconvert tmp.mif ' + cns.templates[cid] + ' -axes 0,1,2,-1') + run.command(f'mrconvert tmp.mif {cns.templates[cid]} -axes 0,1,2,-1') run.function(os.remove, 'tmp.mif') if use_masks: - run.command(['mrmath', path.all_in_dir('mask_transformed')] + - 'min - | maskfilter - median - | '.split() + - ('maskfilter - dilate -npass 5 nl_template_mask' + str(level) + '.mif').split()) - current_template_mask = 'nl_template_mask' + str(level) + '.mif' + current_template_mask = f'nl_template_mask{level}.mif' + run.command(['mrmath', path.all_in_dir('mask_transformed'), 'min', '-', '|', + 'maskfilter', '-', 'median', '-', '|', + 'maskfilter', '-', 'dilate', '-npass', '5', current_template_mask]) if level < len(nl_scales) - 1: if scale < nl_scales[level + 1]: upsample_factor = nl_scales[level + 1] / scale for inp in ins: - run.command('mrgrid ' + os.path.join('warps_%02i' % level, inp.uid + '.mif') + - ' regrid -scale %f tmp.mif' % upsample_factor, force=True) - run.function(shutil.move, 'tmp.mif', os.path.join('warps_%02i' % level, inp.uid + '.mif')) + run.command(['mrgrid', os.path.join(f'warps_{level:02d}', f'{inp.uid}.mif'), + 'regrid', '-scale', str(upsample_factor), 'tmp.mif'], + force=True) + run.function(shutil.move, 'tmp.mif', os.path.join(f'warps_{level:02d}', f'{inp.uid}.mif')) else: for inp in ins: - run.function(shutil.move, os.path.join('warps_%02i' % level, inp.uid + '.mif'), 'warps') + run.function(shutil.move, os.path.join(f'warps_{level:02d}', f'{inp.uid}.mif'), 'warps') progress.done() for cid in range(n_contrasts): - run.command('mrconvert ' + cns.templates[cid] + ' ' + cns.templates_out[cid], - mrconvert_keyval='NULL', force=app.FORCE_OVERWRITE) + run.command(['mrconvert', cns.templates[cid], app.ARGS.template[cid]], + mrconvert_keyval='NULL', + force=app.FORCE_OVERWRITE) if app.ARGS.warp_dir: - warp_path = path.from_user(app.ARGS.warp_dir, False) + warp_path = app.ARGS.warp_dir if os.path.exists(warp_path): run.function(shutil.rmtree, warp_path) os.makedirs(warp_path) - progress = app.ProgressBar('Copying non-linear warps to output directory "' + warp_path + '"', len(ins)) + progress = app.ProgressBar(f'Copying non-linear warps to output directory "{warp_path}"', len(ins)) for inp in ins: - keyval = image.Header(os.path.join('warps', inp.uid + '.mif')).keyval() + keyval = image.Header(os.path.join('warps', f'{inp.uid}.mif')).keyval() keyval = dict((k, keyval[k]) for k in ('linear1', 'linear2')) - json_path = os.path.join('warps', inp.uid + '.json') + json_path = os.path.join('warps', f'{inp.uid}.json') with open(json_path, 'w', encoding='utf-8') as json_file: json.dump(keyval, json_file) - run.command('mrconvert ' + os.path.join('warps', inp.uid + '.mif') + ' ' + - shlex.quote(os.path.join(warp_path, xcontrast_xsubject_pre_postfix[0] + - inp.uid + xcontrast_xsubject_pre_postfix[1] + '.mif')), - mrconvert_keyval=json_path, force=app.FORCE_OVERWRITE) + run.command(['mrconvert', + os.path.join('warps', f'{inp.uid}.mif'), + os.path.join(warp_path, f'{xcontrast_xsubject_pre_postfix[0]}{inp.uid}{xcontrast_xsubject_pre_postfix[1]}.mif')], + mrconvert_keyval=json_path, + force=app.FORCE_OVERWRITE) progress.increment() progress.done() if app.ARGS.linear_transformations_dir: - linear_transformations_path = path.from_user(app.ARGS.linear_transformations_dir, False) + linear_transformations_path = app.ARGS.linear_transformations_dir if os.path.exists(linear_transformations_path): run.function(shutil.rmtree, linear_transformations_path) os.makedirs(linear_transformations_path) for inp in ins: - trafo = matrix.load_transform(os.path.join('linear_transforms', inp.uid + '.txt')) + trafo = matrix.load_transform(os.path.join('linear_transforms', f'{inp.uid}.txt')) matrix.save_transform(os.path.join(linear_transformations_path, - xcontrast_xsubject_pre_postfix[0] + inp.uid - + xcontrast_xsubject_pre_postfix[1] + '.txt'), + f'{xcontrast_xsubject_pre_postfix[0]}{inp.uid}{xcontrast_xsubject_pre_postfix[1]}.txt'), trafo, force=app.FORCE_OVERWRITE) if app.ARGS.transformed_dir: for cid, trdir in enumerate(app.ARGS.transformed_dir): - transformed_path = path.from_user(trdir, False) - if os.path.exists(transformed_path): - run.function(shutil.rmtree, transformed_path) - os.makedirs(transformed_path) - progress = app.ProgressBar('Copying transformed images to output directory "' + transformed_path + '"', len(ins)) + trdir.mkdir() + progress = app.ProgressBar(f'Copying transformed images to output directory {trdir}', len(ins)) for inp in ins: - run.command(['mrconvert', inp.ims_transformed[cid], os.path.join(transformed_path, inp.ims_filenames[cid])], - mrconvert_keyval=inp.get_ims_path(False)[cid], force=app.FORCE_OVERWRITE) + run.command(['mrconvert', inp.ims_transformed[cid], os.path.join(trdir, inp.ims_filenames[cid])], + mrconvert_keyval=inp.get_ims_path(False)[cid], + force=app.FORCE_OVERWRITE) progress.increment() progress.done() if app.ARGS.template_mask: - run.command('mrconvert ' + current_template_mask + ' ' + path.from_user(app.ARGS.template_mask, True), - mrconvert_keyval='NULL', force=app.FORCE_OVERWRITE) + run.command(['mrconvert', current_template_mask, app.ARGS.template_mask], + mrconvert_keyval='NULL', + force=app.FORCE_OVERWRITE) diff --git a/bin/responsemean b/bin/responsemean index f07522e779..627d47e766 100755 --- a/bin/responsemean +++ b/bin/responsemean @@ -16,20 +16,40 @@ # For more details, see http://www.mrtrix.org/. -import math, os, sys +import math, sys def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' + 'and David Raffelt (david.raffelt@florey.edu.au)') cmdline.set_synopsis('Calculate the mean response function from a set of text files') - cmdline.add_description('Example usage: ' + os.path.basename(sys.argv[0]) + ' input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt') - cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), as well as the same number of coefficients per line.') - cmdline.add_description('As long as the number of unique b-values is identical across all input files, the coefficients will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the ' + os.path.basename(sys.argv[0]) + ' command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied.') - cmdline.add_argument('inputs', type=app.Parser.FileIn(), help='The input response function files', nargs='+') - cmdline.add_argument('output', type=app.Parser.FileOut(), help='The output mean response function file') - cmdline.add_argument('-legacy', action='store_true', help='Use the legacy behaviour of former command \'average_response\': average response function coefficients directly, without compensating for global magnitude differences between input files') + cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), ' + 'as well as the same number of coefficients per line.') + cmdline.add_description('As long as the number of unique b-values is identical across all input files, ' + 'the coefficients will be averaged. ' + 'This is performed on the assumption that the actual acquired b-values are identical. ' + 'This is however impossible for the responsemean command to determine based on the data provided; ' + 'it is therefore up to the user to ensure that this requirement is satisfied.') + cmdline.add_example_usage('Usage where all response functions are in the same directory:', + 'responsemean input_response1.txt input_response2.txt input_response3.txt output_average_response.txt') + cmdline.add_example_usage('Usage selecting response functions within a directory using a wildcard:', + 'responsemean input_response*.txt output_average_response.txt') + cmdline.add_example_usage('Usage where data for each participant reside in a participant-specific directory:', + 'responsemean subject-*/response.txt output_average_response.txt') + cmdline.add_argument('inputs', + type=app.Parser.FileIn(), + nargs='+', + help='The input response function files') + cmdline.add_argument('output', + type=app.Parser.FileOut(), + help='The output mean response function file') + cmdline.add_argument('-legacy', + action='store_true', + help='Use the legacy behaviour of former command "average_response": ' + 'average response function coefficients directly, ' + 'without compensating for global magnitude differences between input files') @@ -37,28 +57,34 @@ def execute(): #pylint: disable=unused-variable from mrtrix3 import MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel from mrtrix3 import app, matrix #pylint: disable=no-name-in-module, import-outside-toplevel - app.check_output_path(app.ARGS.output) - data = [ ] # 3D matrix: Subject, b-value, ZSH coefficient for filepath in app.ARGS.inputs: subject = matrix.load_matrix(filepath) if any(len(line) != len(subject[0]) for line in subject[1:]): - raise MRtrixError('File \'' + filepath + '\' does not contain the same number of entries per line (multi-shell response functions must have the same number of coefficients per b-value; pad the data with zeroes if necessary)') + raise MRtrixError(f'File "{filepath}" does not contain the same number of entries per line ' + f'(multi-shell response functions must have the same number of coefficients per b-value; ' + f'pad the data with zeroes if necessary)') if data: if len(subject) != len(data[0]): - raise MRtrixError('File \'' + filepath + '\' contains ' + str(len(subject)) + ' b-value' + ('s' if len(subject) > 1 else '') + ' (line' + ('s' if len(subject) > 1 else '') + '); this differs from the first file read (' + sys.argv[1] + '), which contains ' + str(len(data[0])) + ' line' + ('s' if len(data[0]) > 1 else '')) + raise MRtrixError(f'File "{filepath}" contains {len(subject)} {"b-values" if len(subject) > 1 else "bvalue"}) {"lines" if len(subject) > 1 else ""}; ' + f'this differs from the first file read ({sys.argv[1]}), ' + f'which contains {len(data[0])} {"lines" if len(data[0]) > 1 else ""}') if len(subject[0]) != len(data[0][0]): - raise MRtrixError('File \'' + filepath + '\' contains ' + str(len(subject[0])) + ' coefficient' + ('s' if len(subject[0]) > 1 else '') + ' per b-value (line); this differs from the first file read (' + sys.argv[1] + '), which contains ' + str(len(data[0][0])) + ' coefficient' + ('s' if len(data[0][0]) > 1 else '') + ' per line') + raise MRtrixError(f'File "{filepath}" contains {len(subject[0])} {"coefficients" if len(subject[0]) > 1 else ""} per b-value (line); ' + f'this differs from the first file read ({sys.argv[1]}), ' + f'which contains {len(data[0][0])} {"coefficients" if len(data[0][0]) > 1 else ""} per line') data.append(subject) - app.console('Calculating mean RF across ' + str(len(data)) + ' inputs, with ' + str(len(data[0])) + ' b-value' + ('s' if len(data[0])>1 else '') + ' and lmax=' + str(2*(len(data[0][0])-1))) + app.console(f'Calculating mean RF across {len(data)} inputs, ' + f'with {len(data[0])} {"b-values" if len(data[0])>1 else ""} ' + f'and lmax={2*(len(data[0][0])-1)}') # Old approach: Just take the average across all subjects # New approach: Calculate a multiplier to use for each subject, based on the geometric mean # scaling factor required to bring the subject toward the group mean l=0 terms (across shells) mean_lzero_terms = [ sum(subject[row][0] for subject in data)/len(data) for row in range(len(data[0])) ] - app.debug('Mean l=0 terms: ' + str(mean_lzero_terms)) + app.debug(f'Mean l=0 terms: {mean_lzero_terms}') weighted_sum_coeffs = [[0.0] * len(data[0][0]) for _ in range(len(data[0]))] #pylint: disable=unused-variable for subject in data: @@ -71,8 +97,8 @@ def execute(): #pylint: disable=unused-variable log_multiplier += math.log(mean_lzero / subj_lzero) log_multiplier /= len(data[0]) multiplier = math.exp(log_multiplier) - app.debug('Subject l=0 terms: ' + str(subj_lzero_terms)) - app.debug('Resulting multipler: ' + str(multiplier)) + app.debug(f'Subject l=0 terms: {subj_lzero_terms}') + app.debug(f'Resulting multipler: {multiplier}') weighted_sum_coeffs = [ [ a + multiplier*b for a, b in zip(linea, lineb) ] for linea, lineb in zip(weighted_sum_coeffs, subject) ] mean_coeffs = [ [ f/len(data) for f in line ] for line in weighted_sum_coeffs ] diff --git a/lib/mrtrix3/_5ttgen/freesurfer.py b/lib/mrtrix3/_5ttgen/freesurfer.py index 579a967b51..d5cde582ca 100644 --- a/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/lib/mrtrix3/_5ttgen/freesurfer.py @@ -23,35 +23,39 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('freesurfer', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate the 5TT image based on a FreeSurfer parcellation image') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input FreeSurfer parcellation image (any image containing \'aseg\' in its name)') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') - options = parser.add_argument_group('Options specific to the \'freesurfer\' algorithm') - options.add_argument('-lut', type=app.Parser.FileIn(), metavar='file', help='Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt)') - - - -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - - -def get_inputs(): #pylint: disable=unused-variable - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif'), - preserve_pipes=True) - if app.ARGS.lut: - run.function(shutil.copyfile, path.from_user(app.ARGS.lut, False), path.to_scratch('LUT.txt', False)) + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input FreeSurfer parcellation image ' + '(any image containing "aseg" in its name)') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output 5TT image') + options = parser.add_argument_group('Options specific to the "freesurfer" algorithm') + options.add_argument('-lut', + type=app.Parser.FileIn(), + metavar='file', + help='Manually provide path to the lookup table on which the input parcellation image is based ' + '(e.g. FreeSurferColorLUT.txt)') def execute(): #pylint: disable=unused-variable + run.command(['mrconvert', app.ARGS.input, 'input.mif'], + preserve_pipes=True) lut_input_path = 'LUT.txt' - if not os.path.exists('LUT.txt'): + if app.ARGS.lut: + run.function(shutil.copyfile, app.ARGS.lut, lut_input_path) + else: freesurfer_home = os.environ.get('FREESURFER_HOME', '') if not freesurfer_home: - raise MRtrixError('Environment variable FREESURFER_HOME is not set; please run appropriate FreeSurfer configuration script, set this variable manually, or provide script with path to file FreeSurferColorLUT.txt using -lut option') + raise MRtrixError('Environment variable FREESURFER_HOME is not set; ' + 'please run appropriate FreeSurfer configuration script, ' + 'set this variable manually, ' + 'or provide script with path to file FreeSurferColorLUT.txt using -lut option') lut_input_path = os.path.join(freesurfer_home, 'FreeSurferColorLUT.txt') if not os.path.isfile(lut_input_path): - raise MRtrixError('Could not find FreeSurfer lookup table file (expected location: ' + lut_input_path + '), and none provided using -lut') + raise MRtrixError(f'Could not find FreeSurfer lookup table file ' + f'(expected location: {lut_input_path}), and none provided using -lut') if app.ARGS.sgm_amyg_hipp: lut_output_file_name = 'FreeSurfer2ACT_sgm_amyg_hipp.txt' @@ -59,28 +63,31 @@ def execute(): #pylint: disable=unused-variable lut_output_file_name = 'FreeSurfer2ACT.txt' lut_output_path = os.path.join(path.shared_data_path(), path.script_subdir_name(), lut_output_file_name) if not os.path.isfile(lut_output_path): - raise MRtrixError('Could not find lookup table file for converting FreeSurfer parcellation output to tissues (expected location: ' + lut_output_path + ')') + raise MRtrixError(f'Could not find lookup table file for converting FreeSurfer parcellation output to tissues ' + f'(expected location: {lut_output_path})') # Initial conversion from FreeSurfer parcellation to five principal tissue types - run.command('labelconvert input.mif ' + lut_input_path + ' ' + lut_output_path + ' indices.mif') + run.command(f'labelconvert input.mif {lut_input_path} {lut_output_path} indices.mif') # Crop to reduce file size if app.ARGS.nocrop: image = 'indices.mif' else: image = 'indices_cropped.mif' - run.command('mrthreshold indices.mif - -abs 0.5 | mrgrid indices.mif crop ' + image + ' -mask -') + run.command(f'mrthreshold indices.mif - -abs 0.5 | ' + f'mrgrid indices.mif crop {image} -mask -') # Convert into the 5TT format for ACT - run.command('mrcalc ' + image + ' 1 -eq cgm.mif') - run.command('mrcalc ' + image + ' 2 -eq sgm.mif') - run.command('mrcalc ' + image + ' 3 -eq wm.mif') - run.command('mrcalc ' + image + ' 4 -eq csf.mif') - run.command('mrcalc ' + image + ' 5 -eq path.mif') + run.command(f'mrcalc {image} 1 -eq cgm.mif') + run.command(f'mrcalc {image} 2 -eq sgm.mif') + run.command(f'mrcalc {image} 3 -eq wm.mif') + run.command(f'mrcalc {image} 4 -eq csf.mif') + run.command(f'mrcalc {image} 5 -eq path.mif') - run.command('mrcat cgm.mif sgm.mif wm.mif csf.mif path.mif - -axis 3 | mrconvert - result.mif -datatype float32') + run.command('mrcat cgm.mif sgm.mif wm.mif csf.mif path.mif - -axis 3 | ' + 'mrconvert - result.mif -datatype float32') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/_5ttgen/fsl.py b/lib/mrtrix3/_5ttgen/fsl.py index dc34bce720..e9dab101da 100644 --- a/lib/mrtrix3/_5ttgen/fsl.py +++ b/lib/mrtrix3/_5ttgen/fsl.py @@ -15,7 +15,7 @@ import math, os, shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, fsl, image, path, run, utils +from mrtrix3 import app, fsl, image, run, utils @@ -23,47 +23,70 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('fsl', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use FSL commands to generate the 5TT image based on a T1-weighted image') - parser.add_citation('Smith, S. M. Fast robust automated brain extraction. Human Brain Mapping, 2002, 17, 143-155', is_external=True) - parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. IEEE Transactions on Medical Imaging, 2001, 20, 45-57', is_external=True) - parser.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. A Bayesian model of shape and appearance for subcortical brain segmentation. NeuroImage, 2011, 56, 907-922', is_external=True) - parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input T1-weighted image') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') - options = parser.add_argument_group('Options specific to the \'fsl\' algorithm') - options.add_argument('-t2', type=app.Parser.ImageIn(), metavar='image', help='Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST') - options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', help='Manually provide a brain mask, rather than deriving one in the script') - options.add_argument('-premasked', action='store_true', help='Indicate that brain masking has already been applied to the input image') + parser.add_citation('Smith, S. M. ' + 'Fast robust automated brain extraction. ' + 'Human Brain Mapping, 2002, 17, 143-155', + is_external=True) + parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. ' + 'Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. ' + 'IEEE Transactions on Medical Imaging, 2001, 20, 45-57', + is_external=True) + parser.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. ' + 'A Bayesian model of shape and appearance for subcortical brain segmentation. ' + 'NeuroImage, 2011, 56, 907-922', + is_external=True) + parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. ' + 'Advances in functional and structural MR image analysis and implementation as FSL. ' + 'NeuroImage, 2004, 23, S208-S219', + is_external=True) + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input T1-weighted image') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output 5TT image') + options = parser.add_argument_group('Options specific to the "fsl" algorithm') + options.add_argument('-t2', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide a T2-weighted image in addition to the default T1-weighted image; ' + 'this will be used as a second input to FSL FAST') + options.add_argument('-mask', + type=app.Parser.ImageIn(), + metavar='image', + help='Manually provide a brain mask, ' + 'rather than deriving one in the script') + options.add_argument('-premasked', + action='store_true', + help='Indicate that brain masking has already been applied to the input image') parser.flag_mutually_exclusive_options( [ 'mask', 'premasked' ] ) -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - - def get_inputs(): #pylint: disable=unused-variable - image.check_3d_nonunity(path.from_user(app.ARGS.input, False)) - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif'), + image.check_3d_nonunity(app.ARGS.input) + run.command(['mrconvert', app.ARGS.input, app.ScratchPath('input.mif')], preserve_pipes=True) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit -strides -1,+2,+3', + run.command(['mrconvert', app.ARGS.mask, app.ScratchPath('mask.mif'), '-datatype', 'bit', '-strides', '-1,+2,+3'], preserve_pipes=True) if app.ARGS.t2: - if not image.match(path.from_user(app.ARGS.input, False), path.from_user(app.ARGS.t2, False)): - raise MRtrixError('Provided T2 image does not match input T1 image') - run.command('mrconvert ' + path.from_user(app.ARGS.t2) + ' ' + path.to_scratch('T2.nii') + ' -strides -1,+2,+3', + if not image.match(app.ARGS.input, app.ARGS.t2): + raise MRtrixError('Provided T2w image does not match input T1w image') + run.command(['mrconvert', app.ARGS.t2, app.ScratchPath('T2.nii'), '-strides', '-1,+2,+3'], preserve_pipes=True) def execute(): #pylint: disable=unused-variable if utils.is_windows(): - raise MRtrixError('\'fsl\' algorithm of 5ttgen script cannot be run on Windows: FSL not available on Windows') + raise MRtrixError('"fsl" algorithm of 5ttgen script cannot be run on Windows: ' + 'FSL not available on Windows') fsl_path = os.environ.get('FSLDIR', '') if not fsl_path: - raise MRtrixError('Environment variable FSLDIR is not set; please run appropriate FSL configuration script') + raise MRtrixError('Environment variable FSLDIR is not set; ' + 'please run appropriate FSL configuration script') bet_cmd = fsl.exe_name('bet') fast_cmd = fsl.exe_name('fast') @@ -72,12 +95,25 @@ def execute(): #pylint: disable=unused-variable first_atlas_path = os.path.join(fsl_path, 'data', 'first', 'models_336_bin') if not os.path.isdir(first_atlas_path): - raise MRtrixError('Atlases required for FSL\'s FIRST program not installed; please install fsl-first-data using your relevant package manager') + raise MRtrixError('Atlases required for FSL\'s FIRST program not installed; ' + 'please install fsl-first-data using your relevant package manager') fsl_suffix = fsl.suffix() - if not app.ARGS.mask and not app.ARGS.premasked and not shutil.which('dc'): - app.warn('Unix command "dc" not found; FSL script "standard_space_roi" may fail') + image.check_3d_nonunity(app.ARGS.input) + run.command(['mrconvert', app.ARGS.input, 'input.mif'], + preserve_pipes=True) + if app.ARGS.mask: + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit', '-strides', '-1,+2,+3'], + preserve_pipes=True) + elif not app.ARGS.premasked and not shutil.which('dc'): + app.warn('Unix command "dc" not found; ' + 'FSL script "standard_space_roi" may fail') + if app.ARGS.t2: + if not image.match('input.mif', app.ARGS.t2): + raise MRtrixError('Provided T2w image does not match input T1w image') + run.command(['mrconvert', app.ARGS.t2, 'T2.nii', '-strides', '-1,+2,+3'], + preserve_pipes=True) sgm_structures = [ 'L_Accu', 'R_Accu', 'L_Caud', 'R_Caud', 'L_Pall', 'R_Pall', 'L_Puta', 'R_Puta', 'L_Thal', 'R_Thal' ] if app.ARGS.sgm_amyg_hipp: @@ -87,9 +123,9 @@ def execute(): #pylint: disable=unused-variable upsample_for_first = False # If voxel size is 1.25mm or larger, make a guess that the user has erroneously re-gridded their data if math.pow(t1_spacing[0] * t1_spacing[1] * t1_spacing[2], 1.0/3.0) > 1.225: - app.warn('Voxel size larger than expected for T1-weighted images (' + str(t1_spacing) + '); ' - 'note that ACT does not require re-gridding of T1 image to DWI space, and indeed ' - 'retaining the original higher resolution of the T1 image is preferable') + app.warn(f'Voxel size larger than expected for T1-weighted images ({t1_spacing}); ' + f'note that ACT does not require re-gridding of T1 image to DWI space, and indeed ' + f'retaining the original higher resolution of the T1 image is preferable') upsample_for_first = True run.command('mrconvert input.mif T1.nii -strides -1,+2,+3') @@ -100,21 +136,21 @@ def execute(): #pylint: disable=unused-variable # Decide whether or not we're going to do any brain masking if app.ARGS.mask: - fast_t1_input = 'T1_masked' + fsl_suffix + fast_t1_input = f'T1_masked{fsl_suffix}' # Check to see if the mask matches the T1 image if image.match('T1.nii', 'mask.mif'): - run.command('mrcalc T1.nii mask.mif -mult ' + fast_t1_input) + run.command(f'mrcalc T1.nii mask.mif -mult {fast_t1_input}') mask_path = 'mask.mif' else: app.warn('Mask image does not match input image - re-gridding') run.command('mrtransform mask.mif mask_regrid.mif -template T1.nii -datatype bit') - run.command('mrcalc T1.nii mask_regrid.mif -mult ' + fast_t1_input) + run.command(f'mrcalc T1.nii mask_regrid.mif -mult {fast_t1_input}') mask_path = 'mask_regrid.mif' if os.path.exists('T2.nii'): - fast_t2_input = 'T2_masked' + fsl_suffix - run.command('mrcalc T2.nii ' + mask_path + ' -mult ' + fast_t2_input) + fast_t2_input = f'T2_masked{fsl_suffix}' + run.command(f'mrcalc T2.nii {mask_path} -mult {fast_t2_input}') elif app.ARGS.premasked: @@ -137,98 +173,97 @@ def execute(): #pylint: disable=unused-variable mni_mask_dilation = 2 try: if mni_mask_dilation: - run.command('maskfilter ' + mni_mask_path + ' dilate mni_mask.nii -npass ' + str(mni_mask_dilation)) + run.command(f'maskfilter {mni_mask_path} dilate mni_mask.nii -npass {mni_mask_dilation}') if app.ARGS.nocrop: ssroi_roi_option = ' -roiNONE' else: ssroi_roi_option = ' -roiFOV' - run.command(ssroi_cmd + ' T1.nii T1_preBET' + fsl_suffix + ' -maskMASK mni_mask.nii' + ssroi_roi_option) + run.command(f'{ssroi_cmd} T1.nii T1_preBET{fsl_suffix} -maskMASK mni_mask.nii {ssroi_roi_option}') else: - run.command(ssroi_cmd + ' T1.nii T1_preBET' + fsl_suffix + ' -b') + run.command(f'{ssroi_cmd} T1.nii T1_preBET{fsl_suffix} -b') except run.MRtrixCmdError: pass try: pre_bet_image = fsl.find_image('T1_preBET') except MRtrixError: - app.warn('FSL script \'standard_space_roi\' did not complete successfully' + \ - ('' if shutil.which('dc') else ' (possibly due to program \'dc\' not being installed') + '; ' + \ + dc_warning = '' if shutil.which('dc') else ' (possibly due to program "dc" not being installed' + app.warn(f'FSL script "standard_space_roi" did not complete successfully{dc_warning}; ' 'attempting to continue by providing un-cropped image to BET') pre_bet_image = 'T1.nii' # BET - run.command(bet_cmd + ' ' + pre_bet_image + ' T1_BET' + fsl_suffix + ' -f 0.15 -R') - fast_t1_input = fsl.find_image('T1_BET' + fsl_suffix) + run.command(f'{bet_cmd} {pre_bet_image} T1_BET{fsl_suffix} -f 0.15 -R') + fast_t1_input = fsl.find_image(f'T1_BET{fsl_suffix}') if os.path.exists('T2.nii'): if app.ARGS.nocrop: fast_t2_input = 'T2.nii' else: # Just a reduction of FoV, no sub-voxel interpolation going on - run.command('mrtransform T2.nii T2_cropped.nii -template ' + fast_t1_input + ' -interp nearest') + run.command(f'mrtransform T2.nii T2_cropped.nii -template {fast_t1_input} -interp nearest') fast_t2_input = 'T2_cropped.nii' # Finish branching based on brain masking # FAST if fast_t2_input: - run.command(fast_cmd + ' -S 2 ' + fast_t2_input + ' ' + fast_t1_input) + run.command(f'{fast_cmd} -S 2 {fast_t2_input} {fast_t1_input}') else: - run.command(fast_cmd + ' ' + fast_t1_input) + run.command(f'{fast_cmd} {fast_t1_input}') # FIRST first_input = 'T1.nii' if upsample_for_first: - app.warn('Generating 1mm isotropic T1 image for FIRST in hope of preventing failure, since input image is of lower resolution') + app.warn('Generating 1mm isotropic T1 image for FIRST in hope of preventing failure, ' + 'since input image is of lower resolution') run.command('mrgrid T1.nii regrid T1_1mm.nii -voxel 1.0 -interp sinc') first_input = 'T1_1mm.nii' - first_brain_extracted_option = '' - if app.ARGS.premasked: - first_brain_extracted_option = ' -b' - first_debug_option = '' - if not app.DO_CLEANUP: - first_debug_option = ' -d' - first_verbosity_option = '' - if app.VERBOSITY == 3: - first_verbosity_option = ' -v' - run.command(first_cmd + ' -m none -s ' + ','.join(sgm_structures) + ' -i ' + first_input + ' -o first' + first_brain_extracted_option + first_debug_option + first_verbosity_option) + first_brain_extracted_option = ['-b'] if app.ARGS.premasked else [] + first_debug_option = [] if app.DO_CLEANUP else ['-d'] + first_verbosity_option = ['-v'] if app.VERBOSITY == 3 else [] + run.command([first_cmd, '-m', 'none', '-s', ','.join(sgm_structures), '-i', first_input, '-o', 'first'] + + first_brain_extracted_option + + first_debug_option + + first_verbosity_option) fsl.check_first('first', sgm_structures) # Convert FIRST meshes to partial volume images pve_image_list = [ ] progress = app.ProgressBar('Generating partial volume images for SGM structures', len(sgm_structures)) for struct in sgm_structures: - pve_image_path = 'mesh2voxel_' + struct + '.mif' - vtk_in_path = 'first-' + struct + '_first.vtk' - vtk_temp_path = struct + '.vtk' - run.command('meshconvert ' + vtk_in_path + ' ' + vtk_temp_path + ' -transform first2real ' + first_input) - run.command('mesh2voxel ' + vtk_temp_path + ' ' + fast_t1_input + ' ' + pve_image_path) + pve_image_path = f'mesh2voxel_{struct}.mif' + vtk_in_path = f'first-{struct}_first.vtk' + vtk_temp_path = f'{struct}.vtk' + run.command(['meshconvert', vtk_in_path, vtk_temp_path, '-transform', 'first2real', first_input]) + run.command(['mesh2voxel', vtk_temp_path, fast_t1_input, pve_image_path]) pve_image_list.append(pve_image_path) progress.increment() progress.done() - run.command(['mrmath', pve_image_list, 'sum', '-', '|', \ + run.command(['mrmath', pve_image_list, 'sum', '-', '|', 'mrcalc', '-', '1.0', '-min', 'all_sgms.mif']) # Combine the tissue images into the 5TT format within the script itself fast_output_prefix = fast_t1_input.split('.')[0] - fast_csf_output = fsl.find_image(fast_output_prefix + '_pve_0') - fast_gm_output = fsl.find_image(fast_output_prefix + '_pve_1') - fast_wm_output = fsl.find_image(fast_output_prefix + '_pve_2') + fast_csf_output = fsl.find_image(f'{fast_output_prefix}_pve_0') + fast_gm_output = fsl.find_image(f'{fast_output_prefix}_pve_1') + fast_wm_output = fsl.find_image(f'{fast_output_prefix}_pve_2') # Step 1: Run LCC on the WM image - run.command('mrthreshold ' + fast_wm_output + ' - -abs 0.001 | ' - 'maskfilter - connect - -connectivity | ' - 'mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') + run.command(f'mrthreshold {fast_wm_output} - -abs 0.001 | ' + f'maskfilter - connect - -connectivity | ' + f'mrcalc 1 - 1 -gt -sub remove_unconnected_wm_mask.mif -datatype bit') # Step 2: Generate the images in the same fashion as the old 5ttgen binary used to: # - Preserve CSF as-is # - Preserve SGM, unless it results in a sum of volume fractions greater than 1, in which case clamp # - Multiply the FAST volume fractions of GM and CSF, so that the sum of CSF, SGM, CGM and WM is 1.0 - run.command('mrcalc ' + fast_csf_output + ' remove_unconnected_wm_mask.mif -mult csf.mif') + run.command(f'mrcalc {fast_csf_output} remove_unconnected_wm_mask.mif -mult csf.mif') run.command('mrcalc 1.0 csf.mif -sub all_sgms.mif -min sgm.mif') - run.command('mrcalc 1.0 csf.mif sgm.mif -add -sub ' + fast_gm_output + ' ' + fast_wm_output + ' -add -div multiplier.mif') + run.command(f'mrcalc 1.0 csf.mif sgm.mif -add -sub {fast_gm_output} {fast_wm_output} -add -div multiplier.mif') run.command('mrcalc multiplier.mif -finite multiplier.mif 0.0 -if multiplier_noNAN.mif') - run.command('mrcalc ' + fast_gm_output + ' multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult cgm.mif') - run.command('mrcalc ' + fast_wm_output + ' multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult wm.mif') + run.command(f'mrcalc {fast_gm_output} multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult cgm.mif') + run.command(f'mrcalc {fast_wm_output} multiplier_noNAN.mif -mult remove_unconnected_wm_mask.mif -mult wm.mif') run.command('mrcalc 0 wm.mif -min path.mif') - run.command('mrcat cgm.mif sgm.mif wm.mif csf.mif path.mif - -axis 3 | mrconvert - combined_precrop.mif -strides +2,+3,+4,+1') + run.command('mrcat cgm.mif sgm.mif wm.mif csf.mif path.mif - -axis 3 | ' + 'mrconvert - combined_precrop.mif -strides +2,+3,+4,+1') # Crop to reduce file size (improves caching of image data during tracking) if app.ARGS.nocrop: @@ -238,7 +273,7 @@ def execute(): #pylint: disable=unused-variable 'mrthreshold - - -abs 0.5 | ' 'mrgrid combined_precrop.mif crop result.mif -mask -') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/_5ttgen/gif.py b/lib/mrtrix3/_5ttgen/gif.py index daedd67270..7e823429ff 100644 --- a/lib/mrtrix3/_5ttgen/gif.py +++ b/lib/mrtrix3/_5ttgen/gif.py @@ -15,7 +15,7 @@ import os from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run @@ -23,29 +23,26 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('gif', parents=[base_parser]) parser.set_author('Matteo Mancini (m.mancini@ucl.ac.uk)') parser.set_synopsis('Generate the 5TT image based on a Geodesic Information Flow (GIF) segmentation image') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input Geodesic Information Flow (GIF) segmentation image') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input Geodesic Information Flow (GIF) segmentation image') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output 5TT image') -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - -def check_gif_input(image_path): - dim = image.Header(image_path).size() - if len(dim) < 4: - raise MRtrixError('Image \'' + image_path + '\' does not look like GIF segmentation (less than 4 spatial dimensions)') - if min(dim[:4]) == 1: - raise MRtrixError('Image \'' + image_path + '\' does not look like GIF segmentation (axis with size 1)') - - -def get_inputs(): #pylint: disable=unused-variable - check_gif_input(path.from_user(app.ARGS.input, False)) - run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('input.mif'), +def execute(): #pylint: disable=unused-variable + header = image.Header(app.ARGS.input) + if len(header.size()) < 4: + raise MRtrixError(f'Image "{header.name()}" does not look like GIF segmentation ' + '(less than 4 spatial dimensions)') + if min(header.size()[:4]) == 1: + raise MRtrixError(f'Image "{header.name()}" does not look like GIF segmentation ' + '(axis with size 1)') + run.command(['mrconvert', app.ARGS.input, 'input.mif'], preserve_pipes=True) - -def execute(): #pylint: disable=unused-variable # Generate the images related to each tissue run.command('mrconvert input.mif -coord 3 1 CSF.mif') run.command('mrconvert input.mif -coord 3 2 cGM.mif') @@ -53,7 +50,8 @@ def execute(): #pylint: disable=unused-variable run.command('mrconvert input.mif -coord 3 4 sGM.mif') # Combine WM and subcortical WM into a unique WM image - run.command('mrconvert input.mif - -coord 3 3,5 | mrmath - sum WM.mif -axis 3') + run.command('mrconvert input.mif - -coord 3 3,5 | ' + 'mrmath - sum WM.mif -axis 3') # Create an empty lesion image run.command('mrcalc WM.mif 0 -mul lsn.mif') @@ -64,9 +62,11 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.nocrop: run.function(os.rename, '5tt.mif', 'result.mif') else: - run.command('mrmath 5tt.mif sum - -axis 3 | mrthreshold - - -abs 0.5 | mrgrid 5tt.mif crop result.mif -mask -') + run.command('mrmath 5tt.mif sum - -axis 3 | ' + 'mrthreshold - - -abs 0.5 | ' + 'mrgrid 5tt.mif crop result.mif -mask -') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/_5ttgen/hsvs.py b/lib/mrtrix3/_5ttgen/hsvs.py index 418ba2d732..46b6b82107 100644 --- a/lib/mrtrix3/_5ttgen/hsvs.py +++ b/lib/mrtrix3/_5ttgen/hsvs.py @@ -33,19 +33,58 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('hsvs', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - parser.set_synopsis('Generate a 5TT image based on Hybrid Surface and Volume Segmentation (HSVS), using FreeSurfer and FSL tools') - parser.add_argument('input', type=app.Parser.DirectoryIn(), help='The input FreeSurfer subject directory') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output 5TT image') - parser.add_argument('-template', type=app.Parser.ImageIn(), metavar='image', help='Provide an image that will form the template for the generated 5TT image') - parser.add_argument('-hippocampi', choices=HIPPOCAMPI_CHOICES, help='Select method to be used for hippocampi (& amygdalae) segmentation; options are: ' + ','.join(HIPPOCAMPI_CHOICES)) - parser.add_argument('-thalami', choices=THALAMI_CHOICES, help='Select method to be used for thalamic segmentation; options are: ' + ','.join(THALAMI_CHOICES)) - parser.add_argument('-white_stem', action='store_true', help='Classify the brainstem as white matter') - parser.add_citation('Smith, R.; Skoch, A.; Bajada, C.; Caspers, S.; Connelly, A. Hybrid Surface-Volume Segmentation for improved Anatomically-Constrained Tractography. In Proc OHBM 2020') - parser.add_citation('Fischl, B. Freesurfer. NeuroImage, 2012, 62(2), 774-781', is_external=True) - parser.add_citation('Iglesias, J.E.; Augustinack, J.C.; Nguyen, K.; Player, C.M.; Player, A.; Wright, M.; Roy, N.; Frosch, M.P.; Mc Kee, A.C.; Wald, L.L.; Fischl, B.; and Van Leemput, K. A computational atlas of the hippocampal formation using ex vivo, ultra-high resolution MRI: Application to adaptive segmentation of in vivo MRI. NeuroImage, 2015, 115, 117-137', condition='If FreeSurfer hippocampal subfields module is utilised', is_external=True) - parser.add_citation('Saygin, Z.M. & Kliemann, D.; Iglesias, J.E.; van der Kouwe, A.J.W.; Boyd, E.; Reuter, M.; Stevens, A.; Van Leemput, K.; Mc Kee, A.; Frosch, M.P.; Fischl, B.; Augustinack, J.C. High-resolution magnetic resonance imaging reveals nuclei of the human amygdala: manual segmentation to automatic atlas. NeuroImage, 2017, 155, 370-382', condition='If FreeSurfer hippocampal subfields module is utilised and includes amygdalae segmentation', is_external=True) - parser.add_citation('Iglesias, J.E.; Insausti, R.; Lerma-Usabiaga, G.; Bocchetta, M.; Van Leemput, K.; Greve, D.N.; van der Kouwe, A.; ADNI; Fischl, B.; Caballero-Gaudes, C.; Paz-Alonso, P.M. A probabilistic atlas of the human thalamic nuclei combining ex vivo MRI and histology. NeuroImage, 2018, 183, 314-326', condition='If -thalami nuclei is used', is_external=True) - parser.add_citation('Ardekani, B.; Bachman, A.H. Model-based automatic detection of the anterior and posterior commissures on MRI scans. NeuroImage, 2009, 46(3), 677-682', condition='If ACPCDetect is installed', is_external=True) + parser.set_synopsis('Generate a 5TT image based on Hybrid Surface and Volume Segmentation (HSVS), ' + 'using FreeSurfer and FSL tools') + parser.add_argument('input', + type=app.Parser.DirectoryIn(), + help='The input FreeSurfer subject directory') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output 5TT image') + parser.add_argument('-template', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide an image that will form the template for the generated 5TT image') + parser.add_argument('-hippocampi', + choices=HIPPOCAMPI_CHOICES, + help='Select method to be used for hippocampi (& amygdalae) segmentation; ' + f'options are: {",".join(HIPPOCAMPI_CHOICES)}') + parser.add_argument('-thalami', + choices=THALAMI_CHOICES, + help='Select method to be used for thalamic segmentation; ' + f'options are: {",".join(THALAMI_CHOICES)}') + parser.add_argument('-white_stem', + action='store_true', + help='Classify the brainstem as white matter') + parser.add_citation('Smith, R.; Skoch, A.; Bajada, C.; Caspers, S.; Connelly, A. ' + 'Hybrid Surface-Volume Segmentation for improved Anatomically-Constrained Tractography. ' + 'In Proc OHBM 2020') + parser.add_citation('Fischl, B. ' + 'Freesurfer. ' + 'NeuroImage, 2012, 62(2), 774-781', + is_external=True) + parser.add_citation('Iglesias, J.E.; Augustinack, J.C.; Nguyen, K.; Player, C.M.; Player, A.; Wright, M.; Roy, N.; Frosch, M.P.; Mc Kee, A.C.; Wald, L.L.; Fischl, B.; and Van Leemput, K. ' + 'A computational atlas of the hippocampal formation using ex vivo, ultra-high resolution MRI: ' + 'Application to adaptive segmentation of in vivo MRI. ' + 'NeuroImage, 2015, 115, 117-137', + condition='If FreeSurfer hippocampal subfields module is utilised', + is_external=True) + parser.add_citation('Saygin, Z.M. & Kliemann, D.; Iglesias, J.E.; van der Kouwe, A.J.W.; Boyd, E.; Reuter, M.; Stevens, A.; Van Leemput, K.; Mc Kee, A.; Frosch, M.P.; Fischl, B.; Augustinack, J.C. ' + 'High-resolution magnetic resonance imaging reveals nuclei of the human amygdala: ' + 'manual segmentation to automatic atlas. ' + 'NeuroImage, 2017, 155, 370-382', + condition='If FreeSurfer hippocampal subfields module is utilised and includes amygdalae segmentation', + is_external=True) + parser.add_citation('Iglesias, J.E.; Insausti, R.; Lerma-Usabiaga, G.; Bocchetta, M.; Van Leemput, K.; Greve, D.N.; van der Kouwe, A.; ADNI; Fischl, B.; Caballero-Gaudes, C.; Paz-Alonso, P.M. ' + 'A probabilistic atlas of the human thalamic nuclei combining ex vivo MRI and histology. ' + 'NeuroImage, 2018, 183, 314-326', + condition='If -thalami nuclei is used', + is_external=True) + parser.add_citation('Ardekani, B.; Bachman, A.H. ' + 'Model-based automatic detection of the anterior and posterior commissures on MRI scans. ' + 'NeuroImage, 2009, 46(3), 677-682', + condition='If ACPCDetect is installed', + is_external=True) @@ -127,38 +166,27 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable def check_file(filepath): if not os.path.isfile(filepath): - raise MRtrixError('Required input file missing (expected location: ' + filepath + ')') + raise MRtrixError(f'Required input file missing ' + f'(expected location: {filepath})') def check_dir(dirpath): if not os.path.isdir(dirpath): - raise MRtrixError('Unable to find sub-directory \'' + dirpath + '\' within input directory') + raise MRtrixError(f'Unable to find sub-directory "{dirpath}" within input directory') +def execute(): #pylint: disable=unused-variable -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - - -def get_inputs(): #pylint: disable=unused-variable # Most freeSurfer files will be accessed in-place; no need to pre-convert them into the temporary directory # However convert aparc image so that it does not have to be repeatedly uncompressed - run.command('mrconvert ' + path.from_user(os.path.join(app.ARGS.input, 'mri', 'aparc+aseg.mgz'), True) + ' ' + path.to_scratch('aparc.mif', True), + run.command(['mrconvert', os.path.join(app.ARGS.input, 'mri', 'aparc+aseg.mgz'), 'aparc.mif'], preserve_pipes=True) if app.ARGS.template: - run.command('mrconvert ' + path.from_user(app.ARGS.template, True) + ' ' + path.to_scratch('template.mif', True) + ' -axes 0,1,2', + run.command(['mrconvert', app.ARGS.template, 'template.mif', '-axes', '0,1,2'], preserve_pipes=True) - - -def execute(): #pylint: disable=unused-variable - - subject_dir = os.path.abspath(path.from_user(app.ARGS.input, False)) - if not os.path.isdir(subject_dir): - raise MRtrixError('Input to hsvs algorithm must be a directory') - surf_dir = os.path.join(subject_dir, 'surf') - mri_dir = os.path.join(subject_dir, 'mri') + surf_dir = os.path.join(app.ARGS.input, 'surf') + mri_dir = os.path.join(app.ARGS.input, 'mri') check_dir(surf_dir) check_dir(mri_dir) #aparc_image = os.path.join(mri_dir, 'aparc+aseg.mgz') @@ -177,7 +205,7 @@ def execute(): #pylint: disable=unused-variable # Use brain-extracted, bias-corrected image for FSL tools norm_image = os.path.join(mri_dir, 'norm.mgz') check_file(norm_image) - run.command('mrconvert ' + norm_image + ' T1.nii -stride -1,+2,+3') + run.command(f'mrconvert {norm_image} T1.nii -stride -1,+2,+3') # Verify FAST availability try: fast_cmd = fsl.exe_name('fast') @@ -199,18 +227,20 @@ def execute(): #pylint: disable=unused-variable first_atlas_path = os.path.join(fsl_path, 'data', 'first', 'models_336_bin') have_first = first_cmd and os.path.isdir(first_atlas_path) else: - app.warn('Environment variable FSLDIR is not set; script will run without FSL components') + app.warn('Environment variable FSLDIR is not set; ' + 'script will run without FSL components') - acpc_string = 'anterior ' + ('& posterior commissures' if ATTEMPT_PC else 'commissure') + acpc_string = f'anterior {"& posterior commissures" if ATTEMPT_PC else "commissure"}' have_acpcdetect = bool(shutil.which('acpcdetect')) and 'ARTHOME' in os.environ if have_acpcdetect: if have_fast: - app.console('ACPCdetect and FSL FAST will be used for explicit segmentation of ' + acpc_string) + app.console(f'ACPCdetect and FSL FAST will be used for explicit segmentation of {acpc_string}') else: - app.warn('ACPCdetect is installed, but FSL FAST not found; cannot segment ' + acpc_string) + app.warn(f'ACPCdetect is installed, but FSL FAST not found; ' + f'cannot segment {acpc_string}') have_acpcdetect = False else: - app.warn('ACPCdetect not installed; cannot segment ' + acpc_string) + app.warn(f'ACPCdetect not installed; cannot segment {acpc_string}') # Need to perform a better search for hippocampal subfield output: names & version numbers may change have_hipp_subfields = False @@ -266,19 +296,23 @@ def execute(): #pylint: disable=unused-variable if hippocampi_method: if hippocampi_method == 'subfields': if not have_hipp_subfields: - raise MRtrixError('Could not isolate hippocampal subfields module output (candidate images: ' + str(hipp_subfield_all_images) + ')') + raise MRtrixError(f'Could not isolate hippocampal subfields module output ' + f'(candidate images: {hipp_subfield_all_images})') elif hippocampi_method == 'first': if not have_first: - raise MRtrixError('Cannot use "first" method for hippocampi segmentation; check FSL installation') + raise MRtrixError('Cannot use "first" method for hippocampi segmentation; ' + 'check FSL installation') else: if have_hipp_subfields: hippocampi_method = 'subfields' - app.console('Hippocampal subfields module output detected; will utilise for hippocampi ' - + ('and amygdalae ' if hipp_subfield_has_amyg else '') - + 'segmentation') + app.console('Hippocampal subfields module output detected; ' + 'will utilise for hippocampi' + f'{" and amygdalae" if hipp_subfield_has_amyg else ""}' + ' segmentation') elif have_first: hippocampi_method = 'first' - app.console('No hippocampal subfields module output detected, but FSL FIRST is installed; ' + app.console('No hippocampal subfields module output detected, ' + 'but FSL FIRST is installed; ' 'will utilise latter for hippocampi segmentation') else: hippocampi_method = 'aseg' @@ -287,7 +321,8 @@ def execute(): #pylint: disable=unused-variable if hippocampi_method == 'subfields': if 'FREESURFER_HOME' not in os.environ: - raise MRtrixError('FREESURFER_HOME environment variable not set; required for use of hippocampal subfields module') + raise MRtrixError('FREESURFER_HOME environment variable not set; ' + 'required for use of hippocampal subfields module') freesurfer_lut_file = os.path.join(os.environ['FREESURFER_HOME'], 'FreeSurferColorLUT.txt') check_file(freesurfer_lut_file) hipp_lut_file = os.path.join(path.shared_data_path(), path.script_subdir_name(), 'hsvs', 'HippSubfields.txt') @@ -309,7 +344,8 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Could not find thalamic nuclei module output') elif thalami_method == 'first': if not have_first: - raise MRtrixError('Cannot use "first" method for thalami segmentation; check FSL installation') + raise MRtrixError('Cannot use "first" method for thalami segmentation; ' + 'check FSL installation') else: # Not happy with outputs of thalamic nuclei submodule; default to FIRST if have_first: @@ -341,13 +377,13 @@ def execute(): #pylint: disable=unused-variable # Get the main cerebrum segments; these are already smooth progress = app.ProgressBar('Mapping FreeSurfer cortical reconstruction to partial volume images', 8) for hemi in [ 'lh', 'rh' ]: - for basename in [ hemi+'.white', hemi+'.pial' ]: + for basename in [ f'{hemi}.white', f'{hemi}.pial' ]: filepath = os.path.join(surf_dir, basename) check_file(filepath) - transformed_path = basename + '_realspace.obj' - run.command('meshconvert ' + filepath + ' ' + transformed_path + ' -binary -transform fs2real ' + aparc_image) + transformed_path = f'{basename}_realspace.obj' + run.command(['meshconvert', filepath, transformed_path, '-binary', '-transform', 'fs2real', aparc_image]) progress.increment() - run.command('mesh2voxel ' + transformed_path + ' ' + template_image + ' ' + basename + '.mif') + run.command(['mesh2voxel', transformed_path, template_image, f'{basename}.mif']) app.cleanup(transformed_path) progress.increment() progress.done() @@ -368,27 +404,28 @@ def execute(): #pylint: disable=unused-variable from_aseg.extend(OTHER_SGM_ASEG) progress = app.ProgressBar('Smoothing non-cortical structures segmented by FreeSurfer', len(from_aseg) + 2) for (index, tissue, name) in from_aseg: - init_mesh_path = name + '_init.vtk' - smoothed_mesh_path = name + '.vtk' - run.command('mrcalc ' + aparc_image + ' ' + str(index) + ' -eq - | voxel2mesh - -threshold 0.5 ' + init_mesh_path) - run.command('meshfilter ' + init_mesh_path + ' smooth ' + smoothed_mesh_path) + init_mesh_path = f'{name}_init.vtk' + smoothed_mesh_path = f'{name}.vtk' + run.command(f'mrcalc {aparc_image} {index} -eq - | ' + f'voxel2mesh - -threshold 0.5 {init_mesh_path}') + run.command(['meshfilter', init_mesh_path, 'smooth', smoothed_mesh_path]) app.cleanup(init_mesh_path) - run.command('mesh2voxel ' + smoothed_mesh_path + ' ' + template_image + ' ' + name + '.mif') + run.command(['mesh2voxel', smoothed_mesh_path, template_image, f'{name}.mif']) app.cleanup(smoothed_mesh_path) - tissue_images[tissue-1].append(name + '.mif') + tissue_images[tissue-1].append(f'{name}.mif') progress.increment() # Lateral ventricles are separate as we want to combine with choroid plexus prior to mesh conversion for hemi_index, hemi_name in enumerate(['Left', 'Right']): - name = hemi_name + '_LatVent_ChorPlex' - init_mesh_path = name + '_init.vtk' - smoothed_mesh_path = name + '.vtk' + name = f'{hemi_name}_LatVent_ChorPlex' + init_mesh_path = f'{name}_init.vtk' + smoothed_mesh_path = f'{name}.vtk' run.command('mrcalc ' + ' '.join(aparc_image + ' ' + str(index) + ' -eq' for index, tissue, name in VENTRICLE_CP_ASEG[hemi_index]) + ' -add - | ' - + 'voxel2mesh - -threshold 0.5 ' + init_mesh_path) - run.command('meshfilter ' + init_mesh_path + ' smooth ' + smoothed_mesh_path) + + f'voxel2mesh - -threshold 0.5 {init_mesh_path}') + run.command(['meshfilter', init_mesh_path, 'smooth', smoothed_mesh_path]) app.cleanup(init_mesh_path) - run.command('mesh2voxel ' + smoothed_mesh_path + ' ' + template_image + ' ' + name + '.mif') + run.command(['mesh2voxel', smoothed_mesh_path, template_image, f'{name}.mif']) app.cleanup(smoothed_mesh_path) - tissue_images[3].append(name + '.mif') + tissue_images[3].append(f'{name}.mif') progress.increment() progress.done() @@ -397,18 +434,19 @@ def execute(): #pylint: disable=unused-variable # Combine corpus callosum segments before smoothing progress = app.ProgressBar('Combining and smoothing corpus callosum segmentation', len(CORPUS_CALLOSUM_ASEG) + 3) for (index, name) in CORPUS_CALLOSUM_ASEG: - run.command('mrcalc ' + aparc_image + ' ' + str(index) + ' -eq ' + name + '.mif -datatype bit') + run.command(f'mrcalc {aparc_image} {index} -eq {name}.mif -datatype bit') progress.increment() cc_init_mesh_path = 'combined_corpus_callosum_init.vtk' cc_smoothed_mesh_path = 'combined_corpus_callosum.vtk' - run.command('mrmath ' + ' '.join([ name + '.mif' for (index, name) in CORPUS_CALLOSUM_ASEG ]) + ' sum - | voxel2mesh - -threshold 0.5 ' + cc_init_mesh_path) + run.command(['mrmath', [f'{name}.mif' for (index, name) in CORPUS_CALLOSUM_ASEG ], 'sum', '-', '|', + 'voxel2mesh', '-', '-threshold', '0.5', cc_init_mesh_path]) for name in [ n for _, n in CORPUS_CALLOSUM_ASEG ]: - app.cleanup(name + '.mif') + app.cleanup(f'{name}.mif') progress.increment() - run.command('meshfilter ' + cc_init_mesh_path + ' smooth ' + cc_smoothed_mesh_path) + run.command(['meshfilter', cc_init_mesh_path, 'smooth', cc_smoothed_mesh_path]) app.cleanup(cc_init_mesh_path) progress.increment() - run.command('mesh2voxel ' + cc_smoothed_mesh_path + ' ' + template_image + ' combined_corpus_callosum.mif') + run.command(['mesh2voxel', cc_smoothed_mesh_path, template_image, 'combined_corpus_callosum.mif']) app.cleanup(cc_smoothed_mesh_path) progress.done() tissue_images[2].append('combined_corpus_callosum.mif') @@ -421,25 +459,27 @@ def execute(): #pylint: disable=unused-variable bs_fullmask_path = 'brain_stem_init.mif' bs_cropmask_path = '' progress = app.ProgressBar('Segmenting and cropping brain stem', 5) - run.command('mrcalc ' + aparc_image + ' ' + str(BRAIN_STEM_ASEG[0][0]) + ' -eq ' - + ' -add '.join([ aparc_image + ' ' + str(index) + ' -eq' for index, name in BRAIN_STEM_ASEG[1:] ]) + ' -add ' - + bs_fullmask_path + ' -datatype bit') + run.command(f'mrcalc {aparc_image} {BRAIN_STEM_ASEG[0][0]} -eq ' + + ' -add '.join([ f'{aparc_image} {index} -eq' for index, name in BRAIN_STEM_ASEG[1:] ]) + + f' -add {bs_fullmask_path} -datatype bit') progress.increment() bs_init_mesh_path = 'brain_stem_init.vtk' - run.command('voxel2mesh ' + bs_fullmask_path + ' ' + bs_init_mesh_path) + run.command(['voxel2mesh', bs_fullmask_path, bs_init_mesh_path]) progress.increment() bs_smoothed_mesh_path = 'brain_stem.vtk' - run.command('meshfilter ' + bs_init_mesh_path + ' smooth ' + bs_smoothed_mesh_path) + run.command(['meshfilter', bs_init_mesh_path, 'smooth', bs_smoothed_mesh_path]) app.cleanup(bs_init_mesh_path) progress.increment() - run.command('mesh2voxel ' + bs_smoothed_mesh_path + ' ' + template_image + ' brain_stem.mif') + run.command(['mesh2voxel', bs_smoothed_mesh_path, template_image, 'brain_stem.mif']) app.cleanup(bs_smoothed_mesh_path) progress.increment() fourthventricle_zmin = min(int(line.split()[2]) for line in run.command('maskdump 4th-Ventricle.mif')[0].splitlines()) if fourthventricle_zmin: bs_cropmask_path = 'brain_stem_crop.mif' - run.command('mredit brain_stem.mif - ' + ' '.join([ '-plane 2 ' + str(index) + ' 0' for index in range(0, fourthventricle_zmin) ]) + ' | ' - 'mrcalc brain_stem.mif - -sub 1e-6 -gt ' + bs_cropmask_path + ' -datatype bit') + run.command('mredit brain_stem.mif - ' + + ' '.join([ '-plane 2 ' + str(index) + ' 0' for index in range(0, fourthventricle_zmin) ]) + + ' | ' + + f'mrcalc brain_stem.mif - -sub 1e-6 -gt {bs_cropmask_path} -datatype bit') app.cleanup(bs_fullmask_path) progress.done() @@ -455,21 +495,22 @@ def execute(): #pylint: disable=unused-variable for subfields_lut_file, structure_name in subfields: for hemi, filename in zip([ 'Left', 'Right'], [ prefix + hipp_subfield_image_suffix for prefix in [ 'l', 'r' ] ]): # Extract individual components from image and assign to different tissues - subfields_all_tissues_image = hemi + '_' + structure_name + '_subfields.mif' - run.command('labelconvert ' + os.path.join(mri_dir, filename) + ' ' + freesurfer_lut_file + ' ' + subfields_lut_file + ' ' + subfields_all_tissues_image) + subfields_all_tissues_image = f'{hemi}_{structure_name}_subfields.mif' + run.command(['labelconvert', os.path.join(mri_dir, filename), freesurfer_lut_file, subfields_lut_file, subfields_all_tissues_image]) progress.increment() for tissue in range(0, 5): - init_mesh_path = hemi + '_' + structure_name + '_subfield_' + str(tissue) + '_init.vtk' - smooth_mesh_path = hemi + '_' + structure_name + '_subfield_' + str(tissue) + '.vtk' - subfield_tissue_image = hemi + '_' + structure_name + '_subfield_' + str(tissue) + '.mif' - run.command('mrcalc ' + subfields_all_tissues_image + ' ' + str(tissue+1) + ' -eq - | ' + \ - 'voxel2mesh - ' + init_mesh_path) + init_mesh_path = f'{hemi}_{structure_name}_subfield_{tissue}_init.vtk' + smooth_mesh_path = f'{hemi}_{structure_name}_subfield_{tissue}.vtk' + subfield_tissue_image = f'{hemi}_{structure_name}_subfield_{tissue}.mif' + run.command(f'mrcalc {subfields_all_tissues_image} {tissue+1} -eq - | ' + f'voxel2mesh - {init_mesh_path}') progress.increment() # Since the hippocampal subfields segmentation can include some fine structures, reduce the extent of smoothing - run.command('meshfilter ' + init_mesh_path + ' smooth ' + smooth_mesh_path + ' -smooth_spatial 2 -smooth_influence 2') + run.command(['meshfilter', init_mesh_path, 'smooth', smooth_mesh_path, + '-smooth_spatial', '2', '-smooth_influence', '2']) app.cleanup(init_mesh_path) progress.increment() - run.command('mesh2voxel ' + smooth_mesh_path + ' ' + template_image + ' ' + subfield_tissue_image) + run.command(['mesh2voxel', smooth_mesh_path, template_image, subfield_tissue_image]) app.cleanup(smooth_mesh_path) progress.increment() tissue_images[tissue].append(subfield_tissue_image) @@ -480,23 +521,24 @@ def execute(): #pylint: disable=unused-variable if thalami_method == 'nuclei': progress = app.ProgressBar('Using detected FreeSurfer thalamic nuclei module output', 6) for hemi in ['Left', 'Right']: - thal_mask_path = hemi + '_Thalamus_mask.mif' - init_mesh_path = hemi + '_Thalamus_init.vtk' - smooth_mesh_path = hemi + '_Thalamus.vtk' - thalamus_image = hemi + '_Thalamus.mif' + thal_mask_path = f'{hemi}_Thalamus_mask.mif' + init_mesh_path = f'{hemi}_Thalamus_init.vtk' + smooth_mesh_path = f'{hemi}_Thalamus.vtk' + thalamus_image = f'{hemi}_Thalamus.mif' if hemi == 'Right': - run.command('mrthreshold ' + os.path.join(mri_dir, thal_nuclei_image) + ' -abs 8200 ' + thal_mask_path) + run.command(['mrthreshold', os.path.join(mri_dir, thal_nuclei_image), '-abs', '8200', thal_mask_path]) else: - run.command('mrcalc ' + os.path.join(mri_dir, thal_nuclei_image) + ' 0 -gt ' - + os.path.join(mri_dir, thal_nuclei_image) + ' 8200 -lt ' - + '-mult ' + thal_mask_path) - run.command('voxel2mesh ' + thal_mask_path + ' ' + init_mesh_path) + run.command(['mrcalc', os.path.join(mri_dir, thal_nuclei_image), '0', '-gt', + os.path.join(mri_dir, thal_nuclei_image), '8200', '-lt', + '-mult', thal_mask_path]) + run.command(['voxel2mesh', thal_mask_path, init_mesh_path]) app.cleanup(thal_mask_path) progress.increment() - run.command('meshfilter ' + init_mesh_path + ' smooth ' + smooth_mesh_path + ' -smooth_spatial 2 -smooth_influence 2') + run.command(['meshfilter', init_mesh_path, 'smooth', smooth_mesh_path, + '-smooth_spatial', '2', '-smooth_influence', '2']) app.cleanup(init_mesh_path) progress.increment() - run.command('mesh2voxel ' + smooth_mesh_path + ' ' + template_image + ' ' + thalamus_image) + run.command(['mesh2voxel', smooth_mesh_path, template_image, thalamus_image]) app.cleanup(smooth_mesh_path) progress.increment() tissue_images[1].append(thalamus_image) @@ -513,19 +555,19 @@ def execute(): #pylint: disable=unused-variable from_first = { key: value for key, value in from_first.items() if 'Hippocampus' not in value and 'Amygdala' not in value } if thalami_method != 'first': from_first = { key: value for key, value in from_first.items() if 'Thalamus' not in value } - run.command(first_cmd + ' -s ' + ','.join(from_first.keys()) + ' -i T1.nii -b -o first') + run.command([first_cmd, '-s', ','.join(from_first.keys()), '-i', 'T1.nii', '-b', '-o', 'first']) fsl.check_first('first', from_first.keys()) app.cleanup(glob.glob('T1_to_std_sub.*')) progress = app.ProgressBar('Mapping FIRST segmentations to image', 2*len(from_first)) for key, value in from_first.items(): - vtk_in_path = 'first-' + key + '_first.vtk' - vtk_converted_path = 'first-' + key + '_transformed.vtk' - run.command('meshconvert ' + vtk_in_path + ' ' + vtk_converted_path + ' -transform first2real T1.nii') + vtk_in_path = f'first-{key}_first.vtk' + vtk_converted_path = f'first-{key}_transformed.vtk' + run.command(['meshconvert', vtk_in_path, vtk_converted_path, '-transform', 'first2real', 'T1.nii']) app.cleanup(vtk_in_path) progress.increment() - run.command('mesh2voxel ' + vtk_converted_path + ' ' + template_image + ' ' + value + '.mif') + run.command(['mesh2voxel', vtk_converted_path, template_image, f'{value}.mif']) app.cleanup(vtk_converted_path) - tissue_images[1].append(value + '.mif') + tissue_images[1].append(f'{value}.mif') progress.increment() if not have_fast: app.cleanup('T1.nii') @@ -539,17 +581,17 @@ def execute(): #pylint: disable=unused-variable # ACPCdetect requires input image to be 16-bit # We also want to realign to RAS beforehand so that we can interpret the output voxel locations properly acpcdetect_input_image = 'T1RAS_16b.nii' - run.command('mrconvert ' + norm_image + ' -datatype uint16 -stride +1,+2,+3 ' + acpcdetect_input_image) + run.command(['mrconvert', norm_image, '-datatype', 'uint16', '-stride', '+1,+2,+3', acpcdetect_input_image]) progress.increment() - run.command('acpcdetect -i ' + acpcdetect_input_image) + run.command(['acpcdetect', '-i', acpcdetect_input_image]) progress.increment() # We need the header in order to go from voxel coordinates to scanner coordinates acpcdetect_input_header = image.Header(acpcdetect_input_image) - acpcdetect_output_path = os.path.splitext(acpcdetect_input_image)[0] + '_ACPC.txt' + acpcdetect_output_path = f'{os.path.splitext(acpcdetect_input_image)[0]}_ACPC.txt' app.cleanup(acpcdetect_input_image) with open(acpcdetect_output_path, 'r', encoding='utf-8') as acpc_file: acpcdetect_output_data = acpc_file.read().splitlines() - app.cleanup(glob.glob(os.path.splitext(acpcdetect_input_image)[0] + "*")) + app.cleanup(glob.glob(f'{os.path.splitext(acpcdetect_input_image)[0]}*')) # Need to scan through the contents of this file, # isolating the AC and PC locations ac_voxel = pc_voxel = None @@ -573,20 +615,20 @@ def voxel2scanner(voxel, header): # Generate the mask image within which FAST will be run acpc_prefix = 'ACPC' if ATTEMPT_PC else 'AC' - acpc_mask_image = acpc_prefix + '_FAST_mask.mif' - run.command('mrcalc ' + template_image + ' nan -eq - | ' - 'mredit - ' + acpc_mask_image + ' -scanner ' - '-sphere ' + ','.join(str(value) for value in ac_scanner) + ' 8 1 ' - + ('-sphere ' + ','.join(str(value) for value in pc_scanner) + ' 5 1' if ATTEMPT_PC else '')) + acpc_mask_image = f'{acpc_prefix}_FAST_mask.mif' + run.command(f'mrcalc {template_image} nan -eq - | ' + f'mredit - {acpc_mask_image} -scanner ' + + '-sphere ' + ','.join(map(str, ac_scanner)) + ' 8 1 ' + + ('-sphere ' + ','.join(map(str, pc_scanner)) + ' 5 1' if ATTEMPT_PC else '')) progress.increment() - acpc_t1_masked_image = acpc_prefix + '_T1.nii' - run.command('mrtransform ' + norm_image + ' -template ' + template_image + ' - | ' - 'mrcalc - ' + acpc_mask_image + ' -mult ' + acpc_t1_masked_image) + acpc_t1_masked_image = f'{acpc_prefix}_T1.nii' + run.command(['mrtransform', norm_image, '-template', template_image, '-', '|', + 'mrcalc', '-', acpc_mask_image, '-mult', acpc_t1_masked_image]) app.cleanup(acpc_mask_image) progress.increment() - run.command(fast_cmd + ' -N ' + acpc_t1_masked_image) + run.command([fast_cmd, '-N', acpc_t1_masked_image]) app.cleanup(acpc_t1_masked_image) progress.increment() @@ -595,10 +637,10 @@ def voxel2scanner(voxel, header): # This should involve grabbing just the WM component of these images # Actually, in retrospect, it may be preferable to do the AC PC segmentation # earlier on, and simply add them to the list of WM structures - acpc_wm_image = acpc_prefix + '.mif' - run.command('mrconvert ' + fsl.find_image(acpc_prefix + '_T1_pve_2') + ' ' + acpc_wm_image) + acpc_wm_image = f'{acpc_prefix}.mif' + run.command(['mrconvert', fsl.find_image(f'{acpc_prefix}_T1_pve_2'), acpc_wm_image]) tissue_images[2].append(acpc_wm_image) - app.cleanup(glob.glob(os.path.splitext(acpc_t1_masked_image)[0] + '*')) + app.cleanup(glob.glob(f'{os.path.splitext(acpc_t1_masked_image)[0]}*')) progress.done() @@ -610,27 +652,28 @@ def voxel2scanner(voxel, header): for hemi in [ 'Left-', 'Right-' ]: wm_index = [ index for index, tissue, name in CEREBELLUM_ASEG if name.startswith(hemi) and 'White' in name ][0] gm_index = [ index for index, tissue, name in CEREBELLUM_ASEG if name.startswith(hemi) and 'Cortex' in name ][0] - run.command('mrcalc ' + aparc_image + ' ' + str(wm_index) + ' -eq ' + aparc_image + ' ' + str(gm_index) + ' -eq -add - | ' + \ - 'voxel2mesh - ' + hemi + 'cerebellum_all_init.vtk') + run.command(f'mrcalc {aparc_image} {wm_index} -eq {aparc_image} {gm_index} -eq -add - | ' + f'voxel2mesh - {hemi}cerebellum_all_init.vtk') progress.increment() - run.command('mrcalc ' + aparc_image + ' ' + str(gm_index) + ' -eq - | ' + \ - 'voxel2mesh - ' + hemi + 'cerebellum_grey_init.vtk') + run.command(f'mrcalc {aparc_image} {gm_index} -eq - | ' + f'voxel2mesh - {hemi}cerebellum_grey_init.vtk') progress.increment() for name, tissue in { 'all':2, 'grey':1 }.items(): - run.command('meshfilter ' + hemi + 'cerebellum_' + name + '_init.vtk smooth ' + hemi + 'cerebellum_' + name + '.vtk') - app.cleanup(hemi + 'cerebellum_' + name + '_init.vtk') + run.command(f'meshfilter {hemi}cerebellum_{name}_init.vtk smooth {hemi}cerebellum_{name}.vtk') + app.cleanup(f'{hemi}cerebellum_{name}_init.vtk') progress.increment() - run.command('mesh2voxel ' + hemi + 'cerebellum_' + name + '.vtk ' + template_image + ' ' + hemi + 'cerebellum_' + name + '.mif') - app.cleanup(hemi + 'cerebellum_' + name + '.vtk') + run.command(f'mesh2voxel {hemi}cerebellum_{name}.vtk {template_image} {hemi}cerebellum_{name}.mif') + app.cleanup(f'{hemi}cerebellum_{name}.vtk') progress.increment() - tissue_images[tissue].append(hemi + 'cerebellum_' + name + '.mif') + tissue_images[tissue].append(f'{hemi}cerebellum_{name}.mif') progress.done() # Construct images with the partial volume of each tissue progress = app.ProgressBar('Combining segmentations of all structures corresponding to each tissue type', 5) for tissue in range(0,5): - run.command('mrmath ' + ' '.join(tissue_images[tissue]) + (' brain_stem.mif' if tissue == 2 else '') + ' sum - | mrcalc - 1.0 -min tissue' + str(tissue) + '_init.mif') + run.command('mrmath ' + ' '.join(tissue_images[tissue]) + (' brain_stem.mif' if tissue == 2 else '') + ' sum - | ' + f'mrcalc - 1.0 -min tissue{tissue}_init.mif') app.cleanup(tissue_images[tissue]) progress.increment() progress.done() @@ -644,34 +687,35 @@ def voxel2scanner(voxel, header): tissue_images = [ 'tissue0.mif', 'tissue1.mif', 'tissue2.mif', 'tissue3.mif', 'tissue4.mif' ] run.function(os.rename, 'tissue4_init.mif', 'tissue4.mif') progress.increment() - run.command('mrcalc tissue3_init.mif tissue3_init.mif ' + tissue_images[4] + ' -add 1.0 -sub 0.0 -max -sub 0.0 -max ' + tissue_images[3]) + run.command(f'mrcalc tissue3_init.mif tissue3_init.mif {tissue_images[4]} -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[3]}') app.cleanup('tissue3_init.mif') progress.increment() - run.command('mrmath ' + ' '.join(tissue_images[3:5]) + ' sum tissuesum_34.mif') + run.command(['mrmath', tissue_images[3:5], 'sum', 'tissuesum_34.mif']) progress.increment() - run.command('mrcalc tissue1_init.mif tissue1_init.mif tissuesum_34.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max ' + tissue_images[1]) + run.command(f'mrcalc tissue1_init.mif tissue1_init.mif tissuesum_34.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[1]}') app.cleanup('tissue1_init.mif') app.cleanup('tissuesum_34.mif') progress.increment() - run.command('mrmath ' + tissue_images[1] + ' ' + ' '.join(tissue_images[3:5]) + ' sum tissuesum_134.mif') + run.command(['mrmath', tissue_images[1], tissue_images[3:5], 'sum', 'tissuesum_134.mif']) progress.increment() - run.command('mrcalc tissue2_init.mif tissue2_init.mif tissuesum_134.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max ' + tissue_images[2]) + run.command(f'mrcalc tissue2_init.mif tissue2_init.mif tissuesum_134.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[2]}') app.cleanup('tissue2_init.mif') app.cleanup('tissuesum_134.mif') progress.increment() - run.command('mrmath ' + ' '.join(tissue_images[1:5]) + ' sum tissuesum_1234.mif') + run.command(['mrmath', tissue_images[1:5], 'sum', 'tissuesum_1234.mif']) progress.increment() - run.command('mrcalc tissue0_init.mif tissue0_init.mif tissuesum_1234.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max ' + tissue_images[0]) + run.command(f'mrcalc tissue0_init.mif tissue0_init.mif tissuesum_1234.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[0]}') app.cleanup('tissue0_init.mif') app.cleanup('tissuesum_1234.mif') progress.increment() tissue_sum_image = 'tissuesum_01234.mif' - run.command('mrmath ' + ' '.join(tissue_images) + ' sum ' + tissue_sum_image) + run.command(['mrmath', tissue_images, 'sum', tissue_sum_image]) progress.done() if app.ARGS.template: - run.command('mrtransform ' + mask_image + ' -template template.mif - | mrthreshold - brainmask.mif -abs 0.5') + run.command(['mrtransform', mask_image, '-template', 'template.mif', '-', '|', + 'mrthreshold', '-', 'brainmask.mif', '-abs', '0.5']) mask_image = 'brainmask.mif' @@ -699,18 +743,19 @@ def voxel2scanner(voxel, header): progress = app.ProgressBar('Preparing images of cerebellum for intensity-based segmentation', 9) cerebellar_hemi_pvf_images = [ ] for hemi in [ 'Left', 'Right' ]: - init_mesh_path = hemi + '-Cerebellum-All-Init.vtk' - smooth_mesh_path = hemi + '-Cerebellum-All-Smooth.vtk' - pvf_image_path = hemi + '-Cerebellum-PVF-Template.mif' + init_mesh_path = f'{hemi}-Cerebellum-All-Init.vtk' + smooth_mesh_path = f'{hemi}-Cerebellum-All-Smooth.vtk' + pvf_image_path = f'{hemi}-Cerebellum-PVF-Template.mif' cerebellum_aseg_hemi = [ entry for entry in CEREBELLUM_ASEG if hemi in entry[2] ] - run.command('mrcalc ' + aparc_image + ' ' + str(cerebellum_aseg_hemi[0][0]) + ' -eq ' + \ - ' -add '.join([ aparc_image + ' ' + str(index) + ' -eq' for index, tissue, name in cerebellum_aseg_hemi[1:] ]) + ' -add - | ' + \ - 'voxel2mesh - ' + init_mesh_path) + run.command(f'mrcalc {aparc_image} {cerebellum_aseg_hemi[0][0]} -eq ' + + ' -add '.join([ aparc_image + ' ' + str(index) + ' -eq' for index, tissue, name in cerebellum_aseg_hemi[1:] ]) + + ' -add - | ' + f'voxel2mesh - {init_mesh_path}') progress.increment() - run.command('meshfilter ' + init_mesh_path + ' smooth ' + smooth_mesh_path) + run.command(['meshfilter', init_mesh_path, 'smooth', smooth_mesh_path]) app.cleanup(init_mesh_path) progress.increment() - run.command('mesh2voxel ' + smooth_mesh_path + ' ' + template_image + ' ' + pvf_image_path) + run.command(['mesh2voxel', smooth_mesh_path, template_image, pvf_image_path]) app.cleanup(smooth_mesh_path) cerebellar_hemi_pvf_images.append(pvf_image_path) progress.increment() @@ -718,23 +763,23 @@ def voxel2scanner(voxel, header): # Combine the two hemispheres together into: # - An image in preparation for running FAST # - A combined total partial volume fraction image that will be later used for tissue recombination - run.command('mrcalc ' + ' '.join(cerebellar_hemi_pvf_images) + ' -add 1.0 -min ' + cerebellum_volume_image) + run.command(['mrcalc', cerebellar_hemi_pvf_images, '-add', '1.0', '-min', cerebellum_volume_image]) app.cleanup(cerebellar_hemi_pvf_images) progress.increment() - run.command('mrthreshold ' + cerebellum_volume_image + ' ' + cerebellum_mask_image + ' -abs 1e-6') + run.command(['mrthreshold', cerebellum_volume_image, cerebellum_mask_image, '-abs', '1e-6']) progress.increment() - run.command('mrtransform ' + norm_image + ' -template ' + template_image + ' - | ' + \ - 'mrcalc - ' + cerebellum_mask_image + ' -mult ' + t1_cerebellum_masked) + run.command(['mrtransform', norm_image, '-template', template_image, '-', '|', + 'mrcalc', '-', cerebellum_mask_image, '-mult', t1_cerebellum_masked]) progress.done() else: app.console('Preparing images of cerebellum for intensity-based segmentation') - run.command('mrcalc ' + aparc_image + ' ' + str(CEREBELLUM_ASEG[0][0]) + ' -eq ' + \ - ' -add '.join([ aparc_image + ' ' + str(index) + ' -eq' for index, tissue, name in CEREBELLUM_ASEG[1:] ]) + ' -add ' + \ - cerebellum_volume_image) + run.command(f'mrcalc {aparc_image} {CEREBELLUM_ASEG[0][0]} -eq ' + + ' -add '.join([ f'{aparc_image} {index} -eq' for index, tissue, name in CEREBELLUM_ASEG[1:] ]) + + f' -add {cerebellum_volume_image}') cerebellum_mask_image = cerebellum_volume_image - run.command('mrcalc T1.nii ' + cerebellum_mask_image + ' -mult ' + t1_cerebellum_masked) + run.command(['mrcalc', 'T1.nii', cerebellum_mask_image, '-mult', t1_cerebellum_masked]) app.cleanup('T1.nii') @@ -747,25 +792,25 @@ def voxel2scanner(voxel, header): # FAST memory usage can also be huge when using a high-resolution template image: # Crop T1 image around the cerebellum before feeding to FAST, then re-sample to full template image FoV fast_input_image = 'T1_cerebellum.nii' - run.command('mrgrid ' + t1_cerebellum_masked + ' crop -mask ' + cerebellum_mask_image + ' ' + fast_input_image) + run.command(['mrgrid', t1_cerebellum_masked, 'crop', '-mask', cerebellum_mask_image, fast_input_image]) app.cleanup(t1_cerebellum_masked) # Cleanup of cerebellum_mask_image: # May be same image as cerebellum_volume_image, which is required later if cerebellum_mask_image != cerebellum_volume_image: app.cleanup(cerebellum_mask_image) - run.command(fast_cmd + ' -N ' + fast_input_image) + run.command([fast_cmd, '-N', fast_input_image]) app.cleanup(fast_input_image) # Use glob to clean up unwanted FAST outputs fast_output_prefix = os.path.splitext(fast_input_image)[0] - fast_pve_output_prefix = fast_output_prefix + '_pve_' - app.cleanup([ entry for entry in glob.glob(fast_output_prefix + '*') if not fast_pve_output_prefix in entry ]) + fast_pve_output_prefix = f'{fast_output_prefix}_pve_' + app.cleanup([ entry for entry in glob.glob(f'{fast_output_prefix}*') if not fast_pve_output_prefix in entry ]) progress = app.ProgressBar('Introducing intensity-based cerebellar segmentation into the 5TT image', 10) - fast_outputs_cropped = [ fast_pve_output_prefix + str(n) + fast_suffix for n in range(0,3) ] - fast_outputs_template = [ 'FAST_' + str(n) + '.mif' for n in range(0,3) ] + fast_outputs_cropped = [ f'{fast_pve_output_prefix}{n}{fast_suffix}' for n in range(0,3) ] + fast_outputs_template = [ f'FAST_{n}.mif' for n in range(0,3) ] for inpath, outpath in zip(fast_outputs_cropped, fast_outputs_template): - run.command('mrtransform ' + inpath + ' -interp nearest -template ' + template_image + ' ' + outpath) + run.command(['mrtransform', inpath, '-interp', 'nearest', '-template', template_image, outpath]) app.cleanup(inpath) progress.increment() if app.ARGS.template: @@ -782,29 +827,29 @@ def voxel2scanner(voxel, header): new_tissue_images = [ 'tissue0_fast.mif', 'tissue1_fast.mif', 'tissue2_fast.mif', 'tissue3_fast.mif', 'tissue4_fast.mif' ] new_tissue_sum_image = 'tissuesum_01234_fast.mif' cerebellum_multiplier_image = 'Cerebellar_multiplier.mif' - run.command('mrcalc ' + cerebellum_volume_image + ' ' + tissue_sum_image + ' -add 0.5 -gt 1.0 ' + tissue_sum_image + ' -sub 0.0 -if ' + cerebellum_multiplier_image) + run.command(f'mrcalc {cerebellum_volume_image} {tissue_sum_image} -add 0.5 -gt 1.0 {tissue_sum_image} -sub 0.0 -if {cerebellum_multiplier_image}') app.cleanup(cerebellum_volume_image) progress.increment() - run.command('mrconvert ' + tissue_images[0] + ' ' + new_tissue_images[0]) + run.command(['mrconvert', tissue_images[0], new_tissue_images[0]]) app.cleanup(tissue_images[0]) progress.increment() - run.command('mrcalc ' + tissue_images[1] + ' ' + cerebellum_multiplier_image + ' ' + fast_outputs_template[1] + ' -mult -add ' + new_tissue_images[1]) + run.command(f'mrcalc {tissue_images[1]} {cerebellum_multiplier_image} {fast_outputs_template[1]} -mult -add {new_tissue_images[1]}') app.cleanup(tissue_images[1]) app.cleanup(fast_outputs_template[1]) progress.increment() - run.command('mrcalc ' + tissue_images[2] + ' ' + cerebellum_multiplier_image + ' ' + fast_outputs_template[2] + ' -mult -add ' + new_tissue_images[2]) + run.command(f'mrcalc {tissue_images[2]} {cerebellum_multiplier_image} {fast_outputs_template[2]} -mult -add {new_tissue_images[2]}') app.cleanup(tissue_images[2]) app.cleanup(fast_outputs_template[2]) progress.increment() - run.command('mrcalc ' + tissue_images[3] + ' ' + cerebellum_multiplier_image + ' ' + fast_outputs_template[0] + ' -mult -add ' + new_tissue_images[3]) + run.command(f'mrcalc {tissue_images[3]} {cerebellum_multiplier_image} {fast_outputs_template[0]} -mult -add {new_tissue_images[3]}') app.cleanup(tissue_images[3]) app.cleanup(fast_outputs_template[0]) app.cleanup(cerebellum_multiplier_image) progress.increment() - run.command('mrconvert ' + tissue_images[4] + ' ' + new_tissue_images[4]) + run.command(['mrconvert', tissue_images[4], new_tissue_images[4]]) app.cleanup(tissue_images[4]) progress.increment() - run.command('mrmath ' + ' '.join(new_tissue_images) + ' sum ' + new_tissue_sum_image) + run.command(['mrmath', new_tissue_images, 'sum', new_tissue_sum_image]) app.cleanup(tissue_sum_image) progress.done() tissue_images = new_tissue_images @@ -825,16 +870,16 @@ def voxel2scanner(voxel, header): new_tissue_images = [ tissue_images[0], tissue_images[1], tissue_images[2], - os.path.splitext(tissue_images[3])[0] + '_filled.mif', + f'{os.path.splitext(tissue_images[3])[0]}_filled.mif', tissue_images[4] ] csf_fill_image = 'csf_fill.mif' - run.command('mrcalc 1.0 ' + tissue_sum_image + ' -sub ' + tissue_sum_image + ' 0.0 -gt ' + mask_image + ' -add 1.0 -min -mult 0.0 -max ' + csf_fill_image) + run.command(f'mrcalc 1.0 {tissue_sum_image} -sub {tissue_sum_image} 0.0 -gt {mask_image} -add 1.0 -min -mult 0.0 -max {csf_fill_image}') app.cleanup(tissue_sum_image) # If no template is specified, this file is part of the FreeSurfer output; hence don't modify if app.ARGS.template: app.cleanup(mask_image) progress.increment() - run.command('mrcalc ' + tissue_images[3] + ' ' + csf_fill_image + ' -add ' + new_tissue_images[3]) + run.command(f'mrcalc {tissue_images[3]} {csf_fill_image} -add {new_tissue_images[3]}') app.cleanup(csf_fill_image) app.cleanup(tissue_images[3]) progress.done() @@ -849,16 +894,16 @@ def voxel2scanner(voxel, header): progress = app.ProgressBar('Moving brain stem to volume index 4', 3) new_tissue_images = [ tissue_images[0], tissue_images[1], - os.path.splitext(tissue_images[2])[0] + '_no_brainstem.mif', + f'{os.path.splitext(tissue_images[2])[0]}_no_brainstem.mif', tissue_images[3], - os.path.splitext(tissue_images[4])[0] + '_with_brainstem.mif' ] - run.command('mrcalc ' + tissue_images[2] + ' brain_stem.mif -min brain_stem_white_overlap.mif') + f'{os.path.splitext(tissue_images[4])[0]}_with_brainstem.mif' ] + run.command(f'mrcalc {tissue_images[2]} brain_stem.mif -min brain_stem_white_overlap.mif') app.cleanup('brain_stem.mif') progress.increment() - run.command('mrcalc ' + tissue_images[2] + ' brain_stem_white_overlap.mif -sub ' + new_tissue_images[2]) + run.command(f'mrcalc {tissue_images[2]} brain_stem_white_overlap.mif -sub {new_tissue_images[2]}') app.cleanup(tissue_images[2]) progress.increment() - run.command('mrcalc ' + tissue_images[4] + ' brain_stem_white_overlap.mif -add ' + new_tissue_images[4]) + run.command(f'mrcalc {tissue_images[4]} brain_stem_white_overlap.mif -add {new_tissue_images[4]}') app.cleanup(tissue_images[4]) app.cleanup('brain_stem_white_overlap.mif') progress.done() @@ -870,11 +915,11 @@ def voxel2scanner(voxel, header): app.console('Concatenating tissue volumes into 5TT format') precrop_result_image = '5TT.mif' if bs_cropmask_path: - run.command('mrcat ' + ' '.join(tissue_images) + ' - -axis 3 | ' + \ - '5ttedit - ' + precrop_result_image + ' -none ' + bs_cropmask_path) + run.command(['mrcat', tissue_images, '-', '-axis', '3', '|', + '5ttedit', '-', precrop_result_image, '-none', bs_cropmask_path]) app.cleanup(bs_cropmask_path) else: - run.command('mrcat ' + ' '.join(tissue_images) + ' ' + precrop_result_image + ' -axis 3') + run.command(['mrcat', tissue_images, precrop_result_image, '-axis', '3']) app.cleanup(tissue_images) @@ -885,11 +930,14 @@ def voxel2scanner(voxel, header): else: app.console('Cropping final 5TT image') crop_mask_image = 'crop_mask.mif' - run.command('mrconvert ' + precrop_result_image + ' -coord 3 0,1,2,4 - | mrmath - sum - -axis 3 | mrthreshold - - -abs 0.001 | maskfilter - dilate ' + crop_mask_image) - run.command('mrgrid ' + precrop_result_image + ' crop result.mif -mask ' + crop_mask_image) + run.command(f'mrconvert {precrop_result_image} -coord 3 0,1,2,4 - | ' + f'mrmath - sum - -axis 3 | ' + f'mrthreshold - - -abs 0.001 | ' + f'maskfilter - dilate {crop_mask_image}') + run.command(f'mrgrid {precrop_result_image} crop result.mif -mask {crop_mask_image}') app.cleanup(crop_mask_image) app.cleanup(precrop_result_image) - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(os.path.join(app.ARGS.input, 'mri', 'aparc+aseg.mgz'), True), + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=os.path.join(app.ARGS.input, 'mri', 'aparc+aseg.mgz'), force=app.FORCE_OVERWRITE) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 7d2360d71f..438812560e 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -13,7 +13,7 @@ # # For more details, see http://www.mrtrix.org/. -import argparse, inspect, math, os, random, shlex, shutil, signal, string, subprocess, sys, textwrap, time +import argparse, inspect, math, os, pathlib, random, shlex, shutil, signal, string, subprocess, sys, textwrap, time from mrtrix3 import ANSI, CONFIG, MRtrixError, setup_ansi from mrtrix3 import utils # Needed at global level from ._version import __version__ @@ -183,16 +183,22 @@ def _execute(module): #pylint: disable=unused-variable for keyval in ARGS.config: CONFIG[keyval[0]] = keyval[1] + # Now that FORCE_OVERWRITE has been set, + # check any user-specified output paths + for arg in vars(ARGS): + if isinstance(arg, UserPath): + arg.check_output() + # ANSI settings may have been altered at the command-line setup_ansi() # Check compatibility with command-line piping # if _STDIN_IMAGES and sys.stdin.isatty(): - # sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Piped input images not available from stdin' + ANSI.clear + '\n') + # sys.stderr.write(f{EXEC_NAME}: {ANSI.error}[ERROR] Piped input images not available from stdin{ANSI.clear}\n') # sys.stderr.flush() # sys.exit(1) if _STDOUT_IMAGES and sys.stdout.isatty(): - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Cannot pipe output images as no command connected to stdout' + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] Cannot pipe output images as no command connected to stdout{ANSI.clear}\n') sys.stderr.flush() sys.exit(1) @@ -228,30 +234,30 @@ def _execute(module): #pylint: disable=unused-variable filename = exception_frame[1] lineno = exception_frame[2] sys.stderr.write('\n') - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] ' + (exception.command if is_cmd else exception.function) + ANSI.clear + ' ' + ANSI.debug + '(' + os.path.basename(filename) + ':' + str(lineno) + ')' + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] {exception.command if is_cmd else exception.function}{ANSI.clear} {ANSI.debug}({os.path.basename(filename)}:{lineno}){ANSI.clear}\n') if str(exception): - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Information from failed ' + ('command' if is_cmd else 'function') + ':' + ANSI.clear + '\n') - sys.stderr.write(EXEC_NAME + ':\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] Information from failed {"command" if is_cmd else "function"}:{ANSI.clear}\n') + sys.stderr.write(f'{EXEC_NAME}:\n') for line in str(exception).splitlines(): - sys.stderr.write(' ' * (len(EXEC_NAME)+2) + line + '\n') - sys.stderr.write(EXEC_NAME + ':\n') + sys.stderr.write(f'{" " * (len(EXEC_NAME)+2)}{line}\n') + sys.stderr.write(f'{EXEC_NAME}:\n') else: - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Failed ' + ('command' if is_cmd else 'function') + ' did not provide any output information' + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] Failed {"command" if is_cmd else "function"} did not provide any output information{ANSI.clear}\n') if SCRATCH_DIR: - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] For debugging, inspect contents of scratch directory: ' + SCRATCH_DIR + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] For debugging, inspect contents of scratch directory: {SCRATCH_DIR}{ANSI.clear}\n') sys.stderr.flush() except MRtrixError as exception: return_code = 1 sys.stderr.write('\n') - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] ' + str(exception) + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] {exception}{ANSI.clear}\n') sys.stderr.flush() except Exception as exception: # pylint: disable=broad-except return_code = 1 sys.stderr.write('\n') - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Unhandled Python exception:' + ANSI.clear + '\n') - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR]' + ANSI.clear + ' ' + ANSI.console + type(exception).__name__ + ': ' + str(exception) + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] Unhandled Python exception:{ANSI.clear}\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR]{ANSI.clear} {ANSI.console}{type(exception).__name__}: {exception}{ANSI.clear}\n') traceback = sys.exc_info()[2] - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR] Traceback:' + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] Traceback:{ANSI.clear}\n') for item in inspect.getinnerframes(traceback)[1:]: try: filename = item.filename @@ -263,58 +269,41 @@ def _execute(module): #pylint: disable=unused-variable lineno = item[2] function = item[3] calling_code = item[4] - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR]' + ANSI.clear + ' ' + ANSI.console + filename + ':' + str(lineno) + ' (in ' + function + '())' + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR]{ANSI.clear} {ANSI.console}{filename}:{lineno} (in {function}()){ANSI.clear}\n') for line in calling_code: - sys.stderr.write(EXEC_NAME + ': ' + ANSI.error + '[ERROR]' + ANSI.clear + ' ' + ANSI.debug + line.strip() + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR]{ANSI.clear} {ANSI.debug}{line.strip()}{ANSI.clear}\n') finally: if os.getcwd() != WORKING_DIR: if not return_code: - console('Changing back to original directory (' + WORKING_DIR + ')') + console(f'Changing back to original directory ({WORKING_DIR})') os.chdir(WORKING_DIR) if _STDIN_IMAGES: - debug('Erasing ' + str(len(_STDIN_IMAGES)) + ' piped input images') + debug(f'Erasing {len(_STDIN_IMAGES)} piped input images') for item in _STDIN_IMAGES: try: os.remove(item) - debug('Successfully erased "' + item + '"') + debug(f'Successfully erased "{item}"') except OSError as exc: - debug('Unable to erase "' + item + '": ' + str(exc)) + debug(f'Unable to erase "{item}": {exc}') if SCRATCH_DIR: if DO_CLEANUP: if not return_code: - console('Deleting scratch directory (' + SCRATCH_DIR + ')') + console(f'Deleting scratch directory ({SCRATCH_DIR})') try: shutil.rmtree(SCRATCH_DIR) except OSError: pass SCRATCH_DIR = '' else: - console('Scratch directory retained; location: ' + SCRATCH_DIR) + console(f'Scratch directory retained; location: {SCRATCH_DIR}') if _STDOUT_IMAGES: - debug('Emitting ' + str(len(_STDOUT_IMAGES)) + ' output piped images to stdout') + debug(f'Emitting {len(_STDOUT_IMAGES)} output piped images to stdout') sys.stdout.write('\n'.join(_STDOUT_IMAGES)) sys.exit(return_code) -def check_output_path(item): #pylint: disable=unused-variable - if not item: - return - abspath = os.path.abspath(os.path.join(WORKING_DIR, item)) - if os.path.exists(abspath): - item_type = '' - if os.path.isfile(abspath): - item_type = ' file' - elif os.path.isdir(abspath): - item_type = ' directory' - if FORCE_OVERWRITE: - warn('Output' + item_type + ' \'' + item + '\' already exists; will be overwritten at script completion') - else: - raise MRtrixError('Output' + item_type + ' \'' + item + '\' already exists (use -force to override)') - - - -def make_scratch_dir(): #pylint: disable=unused-variable +def activate_scratch_dir(): #pylint: disable=unused-variable from mrtrix3 import run #pylint: disable=import-outside-toplevel global SCRATCH_DIR if CONTINUE_OPTION: @@ -327,34 +316,28 @@ def make_scratch_dir(): #pylint: disable=unused-variable else: # Defaulting to working directory since too many users have encountered storage issues dir_path = CONFIG.get('ScriptScratchDir', WORKING_DIR) - prefix = CONFIG.get('ScriptScratchPrefix', EXEC_NAME + '-tmp-') + prefix = CONFIG.get('ScriptScratchPrefix', f'{EXEC_NAME}-tmp-') SCRATCH_DIR = dir_path while os.path.isdir(SCRATCH_DIR): random_string = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(6)) - SCRATCH_DIR = os.path.join(dir_path, prefix + random_string) + os.sep + SCRATCH_DIR = os.path.join(dir_path, f'{prefix}{random_string}') + os.sep os.makedirs(SCRATCH_DIR) - console('Generated scratch directory: ' + SCRATCH_DIR) + console(f'Generated scratch directory: {SCRATCH_DIR}') with open(os.path.join(SCRATCH_DIR, 'cwd.txt'), 'w', encoding='utf-8') as outfile: - outfile.write(WORKING_DIR + '\n') + outfile.write(f'{WORKING_DIR}\n') with open(os.path.join(SCRATCH_DIR, 'command.txt'), 'w', encoding='utf-8') as outfile: - outfile.write(' '.join(sys.argv) + '\n') + outfile.write(f'{" ".join(sys.argv)}\n') with open(os.path.join(SCRATCH_DIR, 'log.txt'), 'w', encoding='utf-8'): pass + if VERBOSITY: + console(f'Changing to scratch directory ({SCRATCH_DIR})') + os.chdir(SCRATCH_DIR) # Also use this scratch directory for any piped images within run.command() calls, # and for keeping a log of executed commands / functions run.shared.set_scratch_dir(SCRATCH_DIR) -def goto_scratch_dir(): #pylint: disable=unused-variable - if not SCRATCH_DIR: - raise Exception('No scratch directory location set') - if VERBOSITY: - console('Changing to scratch directory (' + SCRATCH_DIR + ')') - os.chdir(SCRATCH_DIR) - - - # This function can (and should in some instances) be called upon any file / directory # that is no longer required by the script. If the script has been instructed to retain # all intermediates, the resource will be retained; if not, it will be deleted (in particular @@ -367,7 +350,7 @@ def cleanup(items): #pylint: disable=unused-variable cleanup(items[0]) return if VERBOSITY > 2: - console('Cleaning up ' + str(len(items)) + ' intermediate items: ' + str(items)) + console(f'Cleaning up {len(items)} intermediate items: {items}') for item in items: if os.path.isfile(item): func = os.remove @@ -388,14 +371,14 @@ def cleanup(items): #pylint: disable=unused-variable item_type = 'directory' func = shutil.rmtree else: - debug('Unknown target \'' + str(item) + '\'') + debug(f'Unknown target "{item}"') return if VERBOSITY > 2: - console('Cleaning up intermediate ' + item_type + ': \'' + item + '\'') + console(f'Cleaning up intermediate {item_type}: "{item}"') try: func(item) except OSError: - debug('Unable to cleanup intermediate ' + item_type + ': \'' + item + '\'') + debug(f'Unable to cleanup intermediate {item_type}: "{item}"') @@ -405,7 +388,7 @@ def cleanup(items): #pylint: disable=unused-variable # A set of functions and variables for printing various information at the command-line. def console(text): #pylint: disable=unused-variable if VERBOSITY: - sys.stderr.write(EXEC_NAME + ': ' + ANSI.console + text + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.console}{text}{ANSI.clear}\n') def debug(text): #pylint: disable=unused-variable if VERBOSITY <= 2: @@ -414,65 +397,44 @@ def debug(text): #pylint: disable=unused-variable nearest = outer_frames[1] try: if len(outer_frames) == 2: # debug() called directly from script being executed - try: - origin = '(' + os.path.basename(nearest.filename) + ':' + str(nearest.lineno) + ')' - except AttributeError: # Prior to Python 3.5 - origin = '(' + os.path.basename(nearest[1]) + ':' + str(nearest[2]) + ')' + origin = f'({os.path.basename(nearest.filename)}:{nearest.lineno})' else: # Some function has called debug(): Get location of both that function, and where that function was invoked - try: - filename = nearest.filename - funcname = nearest.function + '()' - except AttributeError: # Prior to Python 3.5 - filename = nearest[1] - funcname = nearest[3] + '()' + filename = nearest.filename + funcname = f'{nearest.function}()' modulename = inspect.getmodulename(filename) if modulename: - funcname = modulename + '.' + funcname + funcname = f'{modulename}.{funcname}' origin = funcname caller = outer_frames[2] - try: - origin += ' (from ' + os.path.basename(caller.filename) + ':' + str(caller.lineno) + ')' - except AttributeError: - origin += ' (from ' + os.path.basename(caller[1]) + ':' + str(caller[2]) + ')' - finally: - del caller - sys.stderr.write(EXEC_NAME + ': ' + ANSI.debug + '[DEBUG] ' + origin + ': ' + text + ANSI.clear + '\n') + origin += f' (from {os.path.basename(caller.filename)}:{caller.lineno})' + sys.stderr.write(f'{EXEC_NAME}: {ANSI.debug}[DEBUG] {origin}: {text}{ANSI.clear}\n') finally: del nearest def trace(): #pylint: disable=unused-variable calling_frame = inspect.getouterframes(inspect.currentframe())[1] try: - try: - filename = calling_frame.filename - lineno = calling_frame.lineno - except AttributeError: # Prior to Python 3.5 - filename = calling_frame[1] - lineno = calling_frame[2] - sys.stderr.write(EXEC_NAME + ': at ' + os.path.basename(filename) + ': ' + str(lineno) + '\n') + filename = calling_frame.filename + lineno = calling_frame.lineno + sys.stderr.write(f'{EXEC_NAME}: at {os.path.basename(filename)}:{lineno}\n') finally: del calling_frame def var(*variables): #pylint: disable=unused-variable calling_frame = inspect.getouterframes(inspect.currentframe())[1] try: - try: - calling_code = calling_frame.code_context[0] - filename = calling_frame.filename - lineno = calling_frame.lineno - except AttributeError: # Prior to Python 3.5 - calling_code = calling_frame[4][0] - filename = calling_frame[1] - lineno = calling_frame[2] + calling_code = calling_frame.code_context[0] + filename = calling_frame.filename + lineno = calling_frame.lineno var_string = calling_code[calling_code.find('var(')+4:].rstrip('\n').rstrip(' ')[:-1].replace(',', ' ') var_names, var_values = var_string.split(), variables for name, value in zip(var_names, var_values): - sys.stderr.write(EXEC_NAME + ': [' + os.path.basename(filename) + ': ' + str(lineno) + ']: ' + name + ' = ' + str(value) + '\n') + sys.stderr.write(f'{EXEC_NAME}: [{os.path.basename(filename)}:{lineno}]: {name} = {value}\n') finally: del calling_frame def warn(text): #pylint: disable=unused-variable - sys.stderr.write(EXEC_NAME + ': ' + ANSI.warn + '[WARNING] ' + text + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.warn}[WARNING] {text}{ANSI.clear}\n') @@ -513,9 +475,10 @@ def __init__(self, msg, target=0): if not self.orig_verbosity: return if self.isatty: - sys.stderr.write(self.wrapoff + EXEC_NAME + ': ' + ANSI.execute + '[' + ('{0:>3}%'.format(self.value) if self.multiplier else ProgressBar.BUSY[0]) + ']' + ANSI.clear + ' ' + ANSI.console + self._get_message() + '... ' + ANSI.clear + ANSI.lineclear + self.wrapon + self.newline) + progress_bar = f'{self.value:>3}%' if self.multiplier else ProgressBar.BUSY[0] + sys.stderr.write(f'{self.wrapoff}{EXEC_NAME}: {ANSI.execute}[{progress_bar}]{ANSI.clear} {ANSI.console}{self._get_message()}... {ANSI.clear}{ANSI.lineclear}{self.wrapon}{self.newline}') else: - sys.stderr.write(EXEC_NAME + ': ' + self._get_message() + '... [' + self.newline) + sys.stderr.write(f'{EXEC_NAME}: {self._get_message()}... [{self.newline}') sys.stderr.flush() def increment(self, msg=None): @@ -553,12 +516,13 @@ def done(self, msg=None): if not self.orig_verbosity: return if self.isatty: - sys.stderr.write('\r' + EXEC_NAME + ': ' + ANSI.execute + '[' + ('100%' if self.multiplier else 'done') + ']' + ANSI.clear + ' ' + ANSI.console + self._get_message() + ANSI.clear + ANSI.lineclear + '\n') + progress_bar = '100%' if self.multiplier else 'done' + sys.stderr.write(f'\r{EXEC_NAME}: {ANSI.execute}[{progress_bar}]{ANSI.clear} {ANSI.console}{self._get_message()}{ANSI.clear}{ANSI.lineclear}\n') else: if self.newline: - sys.stderr.write(EXEC_NAME + ': ' + self._get_message() + ' [' + ('=' * int(self.value/2)) + ']\n') + sys.stderr.write(f'{EXEC_NAME}: {self._get_message()} [{"=" * int(self.value/2)}]\n') else: - sys.stderr.write('=' * (int(self.value/2) - int(self.old_value/2)) + ']\n') + sys.stderr.write(f'{"=" * (int(self.value/2) - int(self.old_value/2))}]\n') sys.stderr.flush() @@ -567,10 +531,11 @@ def _update(self): if not self.orig_verbosity: return if self.isatty: - sys.stderr.write(self.wrapoff + '\r' + EXEC_NAME + ': ' + ANSI.execute + '[' + ('{0:>3}%'.format(self.value) if self.multiplier else ProgressBar.BUSY[self.counter%6]) + ']' + ANSI.clear + ' ' + ANSI.console + self._get_message() + '... ' + ANSI.clear + ANSI.lineclear + self.wrapon + self.newline) + progress_bar = f'{self.value:>3}%' if self.multiplier else ProgressBar.BUSY[self.counter%6] + sys.stderr.write(f'{self.wrapoff}\r{EXEC_NAME}: {ANSI.execute}[{progress_bar}]{ANSI.clear} {ANSI.console}{self._get_message()}... {ANSI.clear}{ANSI.lineclear}{self.wrapon}{self.newline}') else: if self.newline: - sys.stderr.write(EXEC_NAME + ': ' + self._get_message() + '... [' + ('=' * int(self.value/2)) + self.newline) + sys.stderr.write(f'{EXEC_NAME}: {self._get_message()}... [{"=" * int(self.value/2)}{self.newline}') else: sys.stderr.write('=' * (int(self.value/2) - int(self.old_value/2))) sys.stderr.flush() @@ -584,6 +549,77 @@ def _get_message(self): +class _FilesystemPath(pathlib.Path): + def __new__(cls, *args, **kwargs): + # TODO Can we use positional arguments rather than kwargs? + root_dir = kwargs.pop('root_dir', None) + assert root_dir is not None + return super().__new__(_WindowsPath if os.name == 'nt' else _PosixPath, + os.path.normpath(os.path.join(root_dir, *args)), + **kwargs) + def __format__(self, _): + return shlex.quote(str(self)) + +class _WindowsPath(pathlib.PureWindowsPath, _FilesystemPath): + _flavour = pathlib._windows_flavour # pylint: disable=protected-access +class _PosixPath(pathlib.PurePosixPath, _FilesystemPath): + _flavour = pathlib._posix_flavour # pylint: disable=protected-access + +class UserPath(_FilesystemPath): + def __new__(cls, *args, **kwargs): + kwargs.update({'root_dir': WORKING_DIR}) + return super().__new__(cls, *args, **kwargs) + def check_output(self): + pass + +class ScratchPath(_FilesystemPath): # pylint: disable=unused-variable + def __new__(cls, *args, **kwargs): + assert SCRATCH_DIR is not None + kwargs.update({'root_dir': SCRATCH_DIR}) + return super().__new__(cls, *args, **kwargs) + +class _UserFileOutPath(UserPath): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + def check_output(self): + if self.exists: + if FORCE_OVERWRITE: + warn(f'Output file path "{str(self)}" already exists; ' + 'will be overwritten at script completion') + else: + raise MRtrixError(f'Output file "{str(self)}" already exists ' + '(use -force to override)') + +class _UserDirOutPath(UserPath): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + def check_output(self): + if self.exists: + if FORCE_OVERWRITE: + warn(f'Output directory path "{str(self)}" already exists; ' + 'will be overwritten at script completion') + else: + raise MRtrixError(f'Output directory "{str(self)}" already exists ' + '(use -force to overwrite)') + def mkdir(self, **kwargs): + # Always force parents=True for user-specified path + parents = kwargs.pop('parents', True) + while True: + if FORCE_OVERWRITE: + try: + shutil.rmtree(self) + except OSError: + pass + try: + super().mkdir(parents=parents, **kwargs) + return + except FileExistsError: + if not FORCE_OVERWRITE: + raise MRtrixError(f'Output directory "{str(self)}" already exists ' + '(use -force to override)') + + + # The Parser class is responsible for setting up command-line parsing for the script. # This includes proper configuration of the argparse functionality, adding standard options # that are common for all scripts, providing a custom help page that is consistent with the @@ -610,7 +646,7 @@ def __call__(self, input_value): try: processed_value = int(processed_value) except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as boolean value"') from exc + raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as boolean value"') from exc return bool(processed_value) @staticmethod def _typestring(): @@ -625,15 +661,15 @@ def __call__(self, input_value): try: value = int(input_value) except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer value') from exc + raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as integer value') from exc if min_value is not None and value < min_value: - raise argparse.ArgumentTypeError('Input value "' + input_value + ' less than minimum permissible value ' + str(min_value)) + raise argparse.ArgumentTypeError(f'Input value "{input_value}" less than minimum permissible value {min_value}') if max_value is not None and value > max_value: - raise argparse.ArgumentTypeError('Input value "' + input_value + ' greater than maximum permissible value ' + str(max_value)) + raise argparse.ArgumentTypeError(f'Input value "{input_value}" greater than maximum permissible value {max_value}') return value @staticmethod def _typestring(): - return 'INT ' + (str(-sys.maxsize - 1) if min_value is None else str(min_value)) + ' ' + (str(sys.maxsize) if max_value is None else str(max_value)) + return f'INT {-sys.maxsize - 1 if min_value is None else min_value} {sys.maxsize if max_value is None else max_value}' return IntChecker() def Float(min_value=None, max_value=None): # pylint: disable=invalid-name @@ -645,15 +681,15 @@ def __call__(self, input_value): try: value = float(input_value) except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point value') from exc + raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as floating-point value') from exc if min_value is not None and value < min_value: - raise argparse.ArgumentTypeError('Input value "' + input_value + ' less than minimum permissible value ' + str(min_value)) + raise argparse.ArgumentTypeError(f'Input value "{input_value}" less than minimum permissible value {min_value}') if max_value is not None and value > max_value: - raise argparse.ArgumentTypeError('Input value "' + input_value + ' greater than maximum permissible value ' + str(max_value)) + raise argparse.ArgumentTypeError(f'Input value "{input_value}" greater than maximum permissible value {max_value}') return value @staticmethod def _typestring(): - return 'FLOAT ' + ('-inf' if min_value is None else str(min_value)) + ' ' + ('inf' if max_value is None else str(max_value)) + return f'FLOAT {"-inf" if min_value is None else str(min_value)} {"inf" if max_value is None else str(max_value)}' return FloatChecker() class SequenceInt(CustomTypeBase): @@ -661,7 +697,7 @@ def __call__(self, input_value): try: return [int(i) for i in input_value.split(',')] except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as integer sequence') from exc + raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as integer sequence') from exc @staticmethod def _typestring(): return 'ISEQ' @@ -671,43 +707,49 @@ def __call__(self, input_value): try: return [float(i) for i in input_value.split(',')] except ValueError as exc: - raise argparse.ArgumentTypeError('Could not interpret "' + input_value + '" as floating-point sequence') from exc + raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as floating-point sequence') from exc @staticmethod def _typestring(): return 'FSEQ' class DirectoryIn(CustomTypeBase): def __call__(self, input_value): - if not os.path.exists(input_value): - raise argparse.ArgumentTypeError('Input directory "' + input_value + '" does not exist') - if not os.path.isdir(input_value): - raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a directory') - return input_value + abspath = UserPath(input_value) + if not abspath.exists(): + raise argparse.ArgumentTypeError(f'Input directory "{input_value}" does not exist') + if not abspath.is_dir(): + raise argparse.ArgumentTypeError(f'Input path "{input_value}" is not a directory') + return abspath @staticmethod def _typestring(): return 'DIRIN' + class DirectoryOut(CustomTypeBase): + def __call__(self, input_value): - return input_value + abspath = _UserDirOutPath(input_value) + return abspath @staticmethod def _typestring(): return 'DIROUT' class FileIn(CustomTypeBase): def __call__(self, input_value): - if not os.path.exists(input_value): - raise argparse.ArgumentTypeError('Input file "' + input_value + '" does not exist') - if not os.path.isfile(input_value): - raise argparse.ArgumentTypeError('Input path "' + input_value + '" is not a file') - return input_value + abspath = UserPath(input_value) + if not abspath.exists(): + raise argparse.ArgumentTypeError(f'Input file "{input_value}" does not exist') + if not abspath.is_file(): + raise argparse.ArgumentTypeError(f'Input path "{input_value}" is not a file') + return abspath @staticmethod def _typestring(): return 'FILEIN' class FileOut(CustomTypeBase): def __call__(self, input_value): - return input_value + abspath = _UserFileOutPath(input_value) + return abspath @staticmethod def _typestring(): return 'FILEOUT' @@ -717,7 +759,8 @@ def __call__(self, input_value): if input_value == '-': input_value = sys.stdin.readline().strip() _STDIN_IMAGES.append(input_value) - return input_value + abspath = UserPath(input_value) + return abspath @staticmethod def _typestring(): return 'IMAGEIN' @@ -727,27 +770,30 @@ def __call__(self, input_value): if input_value == '-': input_value = utils.name_temporary('mif') _STDOUT_IMAGES.append(input_value) - return input_value + # Not guaranteed to catch all cases of output images trying to overwrite existing files; + # but will at least catch some of them + abspath = _UserFileOutPath(input_value) + return abspath @staticmethod def _typestring(): return 'IMAGEOUT' - class TracksIn(FileIn): + class TracksIn(CustomTypeBase): def __call__(self, input_value): - super().__call__(input_value) - if not input_value.endswith('.tck'): - raise argparse.ArgumentTypeError('Input tractogram file "' + input_value + '" is not a valid track file') - return input_value + filepath = Parser.FileIn()(input_value) + if filepath.suffix.lower() != '.tck': + raise argparse.ArgumentTypeError(f'Input tractogram file "{filepath}" is not a valid track file') + return filepath @staticmethod def _typestring(): return 'TRACKSIN' - class TracksOut(FileOut): + class TracksOut(CustomTypeBase): def __call__(self, input_value): - super().__call__(input_value) - if not input_value.endswith('.tck'): - raise argparse.ArgumentTypeError('Output tractogram path "' + input_value + '" does not use the requisite ".tck" suffix') - return input_value + filepath = Parser.FileOut()(input_value) + if filepath.suffix.lower() != '.tck': + raise argparse.ArgumentTypeError(f'Output tractogram path "{filepath}" does not use the requisite ".tck" suffix') + return filepath @staticmethod def _typestring(): return 'TRACKSOUT' @@ -781,25 +827,65 @@ def __init__(self, *args_in, **kwargs_in): self._external_citations = self._external_citations or parent._external_citations else: standard_options = self.add_argument_group('Standard options') - standard_options.add_argument('-info', action='store_true', help='display information messages.') - standard_options.add_argument('-quiet', action='store_true', help='do not display information messages or progress status. Alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string.') - standard_options.add_argument('-debug', action='store_true', help='display debugging messages.') + standard_options.add_argument('-info', + action='store_true', + help='display information messages.') + standard_options.add_argument('-quiet', + action='store_true', + help='do not display information messages or progress status. ' + 'Alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string.') + standard_options.add_argument('-debug', + action='store_true', + help='display debugging messages.') self.flag_mutually_exclusive_options( [ 'info', 'quiet', 'debug' ] ) - standard_options.add_argument('-force', action='store_true', help='force overwrite of output files.') - standard_options.add_argument('-nthreads', metavar='number', type=Parser.Int(0), help='use this number of threads in multi-threaded applications (set to 0 to disable multi-threading).') - standard_options.add_argument('-config', action='append', type=str, metavar=('key', 'value'), nargs=2, help='temporarily set the value of an MRtrix config file entry.') - standard_options.add_argument('-help', action='store_true', help='display this information page and exit.') - standard_options.add_argument('-version', action='store_true', help='display version information and exit.') + standard_options.add_argument('-force', + action='store_true', + help='force overwrite of output files.') + standard_options.add_argument('-nthreads', + metavar='number', + type=Parser.Int(0), + help='use this number of threads in multi-threaded applications ' + '(set to 0 to disable multi-threading).') + standard_options.add_argument('-config', + action='append', + type=str, + metavar=('key', 'value'), + nargs=2, + help='temporarily set the value of an MRtrix config file entry.') + standard_options.add_argument('-help', + action='store_true', + help='display this information page and exit.') + standard_options.add_argument('-version', + action='store_true', + help='display version information and exit.') script_options = self.add_argument_group('Additional standard options for Python scripts') - script_options.add_argument('-nocleanup', action='store_true', help='do not delete intermediate files during script execution, and do not delete scratch directory at script completion.') - script_options.add_argument('-scratch', type=Parser.DirectoryOut(), metavar='/path/to/scratch/', help='manually specify the path in which to generate the scratch directory.') - script_options.add_argument('-continue', type=Parser.Various(), nargs=2, dest='cont', metavar=('ScratchDir', 'LastFile'), help='continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file.') + script_options.add_argument('-nocleanup', + action='store_true', + help='do not delete intermediate files during script execution, ' + 'and do not delete scratch directory at script completion.') + script_options.add_argument('-scratch', + type=Parser.DirectoryOut(), + metavar='/path/to/scratch/', + help='manually specify the path in which to generate the scratch directory.') + script_options.add_argument('-continue', + type=Parser.Various(), + nargs=2, + dest='cont', + metavar=('ScratchDir', 'LastFile'), + help='continue the script from a previous execution; ' + 'must provide the scratch directory path, ' + 'and the name of the last successfully-generated file.') module_file = os.path.realpath (inspect.getsourcefile(inspect.stack()[-1][0])) self._is_project = os.path.abspath(os.path.join(os.path.dirname(module_file), os.pardir, 'lib', 'mrtrix3', 'app.py')) != os.path.abspath(__file__) try: - with subprocess.Popen ([ 'git', 'describe', '--abbrev=8', '--dirty', '--always' ], cwd=os.path.abspath(os.path.join(os.path.dirname(module_file), os.pardir)), stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process: + with subprocess.Popen ([ 'git', 'describe', '--abbrev=8', '--dirty', '--always' ], + cwd=os.path.abspath(os.path.join(os.path.dirname(module_file), os.pardir)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) as process: self._git_version = process.communicate()[0] - self._git_version = str(self._git_version.decode(errors='ignore')).strip() if process.returncode == 0 else 'unknown' + self._git_version = str(self._git_version.decode(errors='ignore')).strip() \ + if process.returncode == 0 \ + else 'unknown' except OSError: self._git_version = 'unknown' @@ -814,7 +900,8 @@ def add_citation(self, citation, **kwargs): #pylint: disable=unused-variable condition = kwargs.pop('condition', None) is_external = kwargs.pop('is_external', False) if kwargs: - raise TypeError('Unsupported keyword arguments passed to app.Parser.add_citation(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to app.Parser.add_citation(): ' + + str(kwargs)) self._citation_list.append( (condition, citation) ) if is_external: self._external_citations = True @@ -882,9 +969,9 @@ def error(self, message): if alg == sys.argv[1]: usage = self._subparsers._group_actions[0].choices[alg].format_usage() continue - sys.stderr.write('\nError: %s\n' % message) - sys.stderr.write('Usage: ' + usage + '\n') - sys.stderr.write(' (Run ' + self.prog + ' -help for more information)\n\n') + sys.stderr.write(f'\nError: {message}\n') + sys.stderr.write(f'Usage: {usage}\n') + sys.stderr.write(f' (Run {self.prog} -help for more information)\n\n') sys.stderr.flush() sys.exit(2) @@ -902,13 +989,13 @@ def _check_mutex_options(self, args_in): count += 1 break if count > 1: - sys.stderr.write('\nError: You cannot use more than one of the following options: ' + ', '.join([ '-' + o for o in group[0] ]) + '\n') - sys.stderr.write('(Consult the help page for more information: ' + self.prog + ' -help)\n\n') + sys.stderr.write(f'\nError: You cannot use more than one of the following options: {", ".join([ "-" + o for o in group[0] ])}\n') + sys.stderr.write(f'(Consult the help page for more information: {self.prog} -help)\n\n') sys.stderr.flush() sys.exit(1) if group[1] and not count: - sys.stderr.write('\nError: One of the following options must be provided: ' + ', '.join([ '-' + o for o in group[0] ]) + '\n') - sys.stderr.write('(Consult the help page for more information: ' + self.prog + ' -help)\n\n') + sys.stderr.write(f'\nError: One of the following options must be provided: {", ".join([ "-" + o for o in group[0] ])}\n') + sys.stderr.write(f'(Consult the help page for more information: {self.prog} -help)\n\n') sys.stderr.flush() sys.exit(1) @@ -929,7 +1016,7 @@ def format_usage(self): argument_list.append(' '.join(arg.metavar)) else: argument_list.append(arg.dest) - return self.prog + ' ' + ' '.join(argument_list) + ' [ options ]' + trailing_ellipsis + return f'{self.prog} {" ".join(argument_list)} [ options ]{trailing_ellipsis}' def print_help(self, file=None): def bold(text): @@ -937,21 +1024,21 @@ def bold(text): def underline(text, ignore_whitespace = True): if not ignore_whitespace: - return ''.join( '_' + chr(0x08) + c for c in text) - return ''.join( '_' + chr(0x08) + c if c != ' ' else c for c in text) + return ''.join('_' + chr(0x08) + c for c in text) + return ''.join('_' + chr(0x08) + c if c != ' ' else c for c in text) wrapper_args = textwrap.TextWrapper(width=80, initial_indent='', subsequent_indent=' ') wrapper_other = textwrap.TextWrapper(width=80, initial_indent=' ', subsequent_indent=' ') if self._is_project: - text = 'Version ' + self._git_version + text = f'Version {self._git_version}' else: - text = 'MRtrix ' + __version__ + text = f'MRtrix {__version__}' text += ' ' * max(1, 40 - len(text) - int(len(self.prog)/2)) text += bold(self.prog) + '\n' if self._is_project: - text += 'using MRtrix3 ' + __version__ + '\n' + text += f'using MRtrix3 {__version__}\n' text += '\n' - text += ' ' + bold(self.prog) + ': ' + ('external MRtrix3 project' if self._is_project else 'part of the MRtrix3 package') + '\n' + text += ' ' + bold(self.prog) + f': {"external MRtrix3 project" if self._is_project else "part of the MRtrix3 package"}\n' text += '\n' text += bold('SYNOPSIS') + '\n' text += '\n' @@ -962,27 +1049,32 @@ def underline(text, ignore_whitespace = True): usage = self.prog + ' ' # Compulsory subparser algorithm selection (if present) if self._subparsers: - usage += self._subparsers._group_actions[0].dest + ' [ options ] ...' + usage += f'{self._subparsers._group_actions[0].dest} [ options ] ...' else: usage += '[ options ]' # Find compulsory input arguments for arg in self._positionals._group_actions: - usage += ' ' + arg.dest + usage += f' {arg.dest}' # Unfortunately this can line wrap early because textwrap is counting each # underlined character as 3 characters when calculating when to wrap # Fix by underlining after the fact text += wrapper_other.fill(usage).replace(self.prog, underline(self.prog), 1) + '\n' text += '\n' if self._subparsers: - text += ' ' + wrapper_args.fill(self._subparsers._group_actions[0].dest + ' '*(max(13-len(self._subparsers._group_actions[0].dest), 1)) + self._subparsers._group_actions[0].help).replace (self._subparsers._group_actions[0].dest, underline(self._subparsers._group_actions[0].dest), 1) + '\n' + text += ' ' + wrapper_args.fill( + self._subparsers._group_actions[0].dest + + ' '*(max(13-len(self._subparsers._group_actions[0].dest), 1)) + + self._subparsers._group_actions[0].help).replace(self._subparsers._group_actions[0].dest, + underline(self._subparsers._group_actions[0].dest), 1) \ + + '\n' text += '\n' for arg in self._positionals._group_actions: line = ' ' if arg.metavar: - name = " ".join(arg.metavar) + name = ' '.join(arg.metavar) else: name = arg.dest - line += name + ' '*(max(13-len(name), 1)) + arg.help + line += f'{name}{" "*(max(13-len(name), 1))}{arg.help}' text += wrapper_args.fill(line).replace(name, underline(name), 1) + '\n' text += '\n' if self._description: @@ -996,8 +1088,10 @@ def underline(text, ignore_whitespace = True): text += '\n' for example in self._examples: for line in wrapper_other.fill(example[0] + ':').splitlines(): - text += ' '*(len(line) - len(line.lstrip())) + underline(line.lstrip(), False) + '\n' - text += ' '*7 + '$ ' + example[1] + '\n' + text += ' '*(len(line) - len(line.lstrip())) \ + + underline(line.lstrip(), False) \ + + '\n' + text += f'{" "*7}$ {example[1]}\n' if example[2]: text += wrapper_other.fill(example[2]) + '\n' text += '\n' @@ -1018,15 +1112,15 @@ def print_group_options(group): group_text += option.metavar elif option.nargs: if isinstance(option.nargs, int): - group_text += (' ' + option.dest.upper())*option.nargs + group_text += (f' {option.dest.upper()}')*option.nargs elif option.nargs in ('+', '*'): group_text += ' ' elif option.nargs == '?': group_text += ' ' elif option.type is not None: - group_text += ' ' + option.type.__name__.upper() + group_text += f' {option.type.__name__.upper()}' elif option.default is None: - group_text += ' ' + option.dest.upper() + group_text += f' {option.dest.upper()}' # Any options that haven't tripped one of the conditions above should be a store_true or store_false, and # therefore there's nothing to be appended to the option instruction if isinstance(option, argparse._AppendAction): @@ -1086,24 +1180,24 @@ def print_full_usage(self): self._subparsers._group_actions[0].choices[alg].print_full_usage() return self.error('Invalid subparser nominated') - sys.stdout.write(self._synopsis + '\n') + sys.stdout.write(f'{self._synopsis}\n') if self._description: if isinstance(self._description, list): for line in self._description: - sys.stdout.write(line + '\n') + sys.stdout.write(f'{line}\n') else: - sys.stdout.write(self._description + '\n') + sys.stdout.write(f'{self._description}\n') for example in self._examples: - sys.stdout.write(example[0] + ': $ ' + example[1]) + sys.stdout.write(f'{example[0]}: $ {example[1]}') if example[2]: - sys.stdout.write('; ' + example[2]) + sys.stdout.write(f'; {example[2]}') sys.stdout.write('\n') def arg2str(arg): if arg.choices: - return 'CHOICE ' + ' '.join(arg.choices) + return f'CHOICE {" ".join(arg.choices)}' if isinstance(arg.type, int) or arg.type is int: - return 'INT ' + str(-sys.maxsize - 1) + ' ' + str(sys.maxsize) + return f'INT {-sys.maxsize - 1} {sys.maxsize}' if isinstance(arg.type, float) or arg.type is float: return 'FLOAT -inf inf' if isinstance(arg.type, str) or arg.type is str or arg.type is None: @@ -1116,34 +1210,23 @@ def allow_multiple(nargs): return '1' if nargs in ('*', '+') else '0' for arg in self._positionals._group_actions: - sys.stdout.write('ARGUMENT ' + arg.dest + ' 0 ' + allow_multiple(arg.nargs) + ' ' + arg2str(arg) + '\n') - sys.stdout.write(arg.help + '\n') + sys.stdout.write(f'ARGUMENT {arg.dest} 0 {allow_multiple(arg.nargs)} {arg2str(arg)}\n') + sys.stdout.write(f'{arg.help}\n') def print_group_options(group): for option in group._group_actions: - sys.stdout.write('OPTION ' - + '/'.join(option.option_strings) - + ' ' - + ('0' if option.required else '1') - + ' ' - + allow_multiple(option.nargs) - + '\n') - sys.stdout.write(option.help + '\n') + sys.stdout.write(f'OPTION {"/".join(option.option_strings)} {"0" if option.required else "1"} {allow_multiple(option.nargs)}\n') + sys.stdout.write(f'{option.help}\n') if option.metavar and isinstance(option.metavar, tuple): assert len(option.metavar) == option.nargs for arg in option.metavar: - sys.stdout.write('ARGUMENT ' + arg + ' 0 0 ' + arg2str(option) + '\n') + sys.stdout.write(f'ARGUMENT {arg} 0 0 {arg2str(option)}\n') else: multiple = allow_multiple(option.nargs) nargs = 1 if multiple == '1' else (option.nargs if option.nargs is not None else 1) for _ in range(0, nargs): - sys.stdout.write('ARGUMENT ' - + (option.metavar if option.metavar else '/'.join(opt.lstrip('-') for opt in option.option_strings)) - + ' 0 ' - + multiple - + ' ' - + arg2str(option) - + '\n') + metavar_string = option.metavar if option.metavar else '/'.join(opt.lstrip('-') for opt in option.option_strings) + sys.stdout.write(f'ARGUMENT {metavar_string} 0 {multiple} {arg2str(option)}\n') ungrouped_options = self._get_ungrouped_options() if ungrouped_options and ungrouped_options._group_actions: @@ -1161,28 +1244,28 @@ def print_usage_markdown(self): return self.error('Invalid subparser nominated') text = '## Synopsis\n\n' - text += self._synopsis + '\n\n' + text += f'{self._synopsis}\n\n' text += '## Usage\n\n' - text += ' ' + self.format_usage() + '\n\n' + text += f' {self.format_usage()}\n\n' if self._subparsers: - text += '- *' + self._subparsers._group_actions[0].dest + '*: ' + self._subparsers._group_actions[0].help + '\n' + text += f'- *{self._subparsers._group_actions[0].dest}*: {self._subparsers._group_actions[0].help}\n' for arg in self._positionals._group_actions: if arg.metavar: name = arg.metavar else: name = arg.dest - text += '- *' + name + '*: ' + arg.help + '\n\n' + text += f'- *{name}*: {arg.help}\n\n' if self._description: text += '## Description\n\n' for line in self._description: - text += line + '\n\n' + text += f'{line}\n\n' if self._examples: text += '## Example usages\n\n' for example in self._examples: - text += '__' + example[0] + ':__\n' - text += '`$ ' + example[1] + '`\n' + text += f'__{example[0]}:__\n' + text += f'`$ {example[1]}`\n' if example[2]: - text += example[2] + '\n' + text += f'{example[2]}\n' text += '\n' text += '## Options\n\n' @@ -1196,10 +1279,10 @@ def print_group_options(group): option_text += ' '.join(option.metavar) else: option_text += option.metavar - group_text += '+ **-' + option_text + '**' + group_text += f'+ **-{option_text}**' if isinstance(option, argparse._AppendAction): group_text += ' *(multiple uses permitted)*' - group_text += '
' + option.help + '\n\n' + group_text += f'
{option.help}\n\n' return group_text ungrouped_options = self._get_ungrouped_options() @@ -1207,24 +1290,27 @@ def print_group_options(group): text += print_group_options(ungrouped_options) for group in reversed(self._action_groups): if self._is_option_group(group): - text += '#### ' + group.title + '\n\n' + text += f'#### {group.title}\n\n' text += print_group_options(group) text += '## References\n\n' for ref in self._citation_list: ref_text = '' if ref[0]: - ref_text += ref[0] + ': ' + ref_text += f'{ref[0]}: ' ref_text += ref[1] - text += ref_text + '\n\n' - text += _MRTRIX3_CORE_REFERENCE + '\n\n' + text += f'{ref_text}\n\n' + text += f'{_MRTRIX3_CORE_REFERENCE}\n\n' text += '---\n\n' - text += '**Author:** ' + self._author + '\n\n' - text += '**Copyright:** ' + self._copyright + '\n\n' + text += f'**Author:** {self._author}\n\n' + text += f'**Copyright:** {self._copyright}\n\n' sys.stdout.write(text) sys.stdout.flush() if self._subparsers: for alg in self._subparsers._group_actions[0].choices: - subprocess.call ([ sys.executable, os.path.realpath(sys.argv[0]), alg, '__print_usage_markdown__' ]) + subprocess.call ([sys.executable, + os.path.realpath(sys.argv[0]), + alg, + '__print_usage_markdown__']) def print_usage_rst(self): # Need to check here whether it's the documentation for a particular subparser that's being requested @@ -1233,39 +1319,40 @@ def print_usage_rst(self): if alg == sys.argv[-2]: self._subparsers._group_actions[0].choices[alg].print_usage_rst() return - self.error('Invalid subparser nominated: ' + sys.argv[-2]) - text = '.. _' + self.prog.replace(' ', '_') + ':\n\n' - text += self.prog + '\n' - text += '='*len(self.prog) + '\n\n' + self.error(f'Invalid subparser nominated: {sys.argv[-2]}') + text = f'.. _{self.prog.replace(" ", "_")}:\n\n' + text += f'{self.prog}\n' + text += f'{"="*len(self.prog)}\n\n' text += 'Synopsis\n' text += '--------\n\n' - text += self._synopsis + '\n\n' + text += f'{self._synopsis}\n\n' text += 'Usage\n' text += '-----\n\n' text += '::\n\n' - text += ' ' + self.format_usage() + '\n\n' + text += f' {self.format_usage()}\n\n' if self._subparsers: - text += '- *' + self._subparsers._group_actions[0].dest + '*: ' + self._subparsers._group_actions[0].help + '\n' + text += f'- *{self._subparsers._group_actions[0].dest}*: {self._subparsers._group_actions[0].help}\n' for arg in self._positionals._group_actions: if arg.metavar: name = arg.metavar else: name = arg.dest - text += '- *' + (' '.join(name) if isinstance(name, tuple) else name) + '*: ' + arg.help.replace('|', '\\|') + '\n' + arg_help = arg.help.replace('|', '\\|') + text += f'- *{" ".join(name) if isinstance(name, tuple) else name}*: {arg_help}\n' text += '\n' if self._description: text += 'Description\n' text += '-----------\n\n' for line in self._description: - text += line + '\n\n' + text += f'{line}\n\n' if self._examples: text += 'Example usages\n' text += '--------------\n\n' for example in self._examples: - text += '- *' + example[0] + '*::\n\n' - text += ' $ ' + example[1] + '\n\n' + text += f'- *{example[0]}*::\n\n' + text += f' $ {example[1]}\n\n' if example[2]: - text += ' ' + example[2] + '\n\n' + text += f' {example[2]}\n\n' text += 'Options\n' text += '-------\n' @@ -1280,10 +1367,11 @@ def print_group_options(group): else: option_text += option.metavar group_text += '\n' - group_text += '- **' + option_text + '**' + group_text += f'- **{option_text}**' if isinstance(option, argparse._AppendAction): group_text += ' *(multiple uses permitted)*' - group_text += ' ' + option.help.replace('|', '\\|') + '\n' + option_help = option.help.replace('|', '\\|') + group_text += f' {option_help}\n' return group_text ungrouped_options = self._get_ungrouped_options() @@ -1292,8 +1380,8 @@ def print_group_options(group): for group in reversed(self._action_groups): if self._is_option_group(group): text += '\n' - text += group.title + '\n' - text += '^'*len(group.title) + '\n' + text += f'{group.title}\n' + text += f'{"^"*len(group.title)}\n' text += print_group_options(group) text += '\n' text += 'References\n' @@ -1301,25 +1389,28 @@ def print_group_options(group): for ref in self._citation_list: ref_text = '* ' if ref[0]: - ref_text += ref[0] + ': ' + ref_text += f'{ref[0]}: ' ref_text += ref[1] - text += ref_text + '\n\n' - text += _MRTRIX3_CORE_REFERENCE + '\n\n' + text += f'{ref_text}\n\n' + text += f'{_MRTRIX3_CORE_REFERENCE}\n\n' text += '--------------\n\n\n\n' - text += '**Author:** ' + self._author + '\n\n' - text += '**Copyright:** ' + self._copyright + '\n\n' + text += f'**Author:** {self._author}\n\n' + text += f'**Copyright:** {self._copyright}\n\n' sys.stdout.write(text) sys.stdout.flush() if self._subparsers: for alg in self._subparsers._group_actions[0].choices: - subprocess.call ([ sys.executable, os.path.realpath(sys.argv[0]), alg, '__print_usage_rst__' ]) + subprocess.call ([sys.executable, + os.path.realpath(sys.argv[0]), + alg, + '__print_usage_rst__']) def print_version(self): - text = '== ' + self.prog + ' ' + (self._git_version if self._is_project else __version__) + ' ==\n' + text = f'== {self.prog} {self._git_version if self._is_project else __version__} ==\n' if self._is_project: - text += 'executing against MRtrix ' + __version__ + '\n' - text += 'Author(s): ' + self._author + '\n' - text += self._copyright + '\n' + text += f'executing against MRtrix {__version__}\n' + text += f'Author(s): {self._author}\n' + text += f'{self._copyright}\n' sys.stdout.write(text) sys.stdout.flush() @@ -1342,17 +1433,24 @@ def _is_option_group(self, group): # Define functions for incorporating commonly-used command-line options / option groups def add_dwgrad_import_options(cmdline): #pylint: disable=unused-variable options = cmdline.add_argument_group('Options for importing the diffusion gradient table') - options.add_argument('-grad', type=Parser.FileIn(), metavar='file', help='Provide the diffusion gradient table in MRtrix format') - options.add_argument('-fslgrad', type=Parser.FileIn(), nargs=2, metavar=('bvecs', 'bvals'), help='Provide the diffusion gradient table in FSL bvecs/bvals format') + options.add_argument('-grad', + type=Parser.FileIn(), + metavar='file', + help='Provide the diffusion gradient table in MRtrix format') + options.add_argument('-fslgrad', + type=Parser.FileIn(), + nargs=2, + metavar=('bvecs', 'bvals'), + help='Provide the diffusion gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'grad', 'fslgrad' ] ) +# TODO Change these to yield lists rather than strings def read_dwgrad_import_options(): #pylint: disable=unused-variable - from mrtrix3 import path #pylint: disable=import-outside-toplevel assert ARGS if ARGS.grad: - return ' -grad ' + path.from_user(ARGS.grad) + return f' -grad {ARGS.grad}' if ARGS.fslgrad: - return ' -fslgrad ' + path.from_user(ARGS.fslgrad[0]) + ' ' + path.from_user(ARGS.fslgrad[1]) + return f' -fslgrad {ARGS.fslgrad[0]} {ARGS.fslgrad[1]}' return '' @@ -1360,22 +1458,25 @@ def read_dwgrad_import_options(): #pylint: disable=unused-variable def add_dwgrad_export_options(cmdline): #pylint: disable=unused-variable options = cmdline.add_argument_group('Options for exporting the diffusion gradient table') - options.add_argument('-export_grad_mrtrix', type=Parser.FileOut(), metavar='grad', help='Export the final gradient table in MRtrix format') - options.add_argument('-export_grad_fsl', type=Parser.FileOut(), nargs=2, metavar=('bvecs', 'bvals'), help='Export the final gradient table in FSL bvecs/bvals format') + options.add_argument('-export_grad_mrtrix', + type=Parser.FileOut(), + metavar='grad', + help='Export the final gradient table in MRtrix format') + options.add_argument('-export_grad_fsl', + type=Parser.FileOut(), + nargs=2, + metavar=('bvecs', 'bvals'), + help='Export the final gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'export_grad_mrtrix', 'export_grad_fsl' ] ) def read_dwgrad_export_options(): #pylint: disable=unused-variable - from mrtrix3 import path #pylint: disable=import-outside-toplevel assert ARGS if ARGS.export_grad_mrtrix: - check_output_path(path.from_user(ARGS.export_grad_mrtrix, False)) - return ' -export_grad_mrtrix ' + path.from_user(ARGS.export_grad_mrtrix) + return f' -export_grad_mrtrix {ARGS.export_grad_mrtrix}' if ARGS.export_grad_fsl: - check_output_path(path.from_user(ARGS.export_grad_fsl[0], False)) - check_output_path(path.from_user(ARGS.export_grad_fsl[1], False)) - return ' -export_grad_fsl ' + path.from_user(ARGS.export_grad_fsl[0]) + ' ' + path.from_user(ARGS.export_grad_fsl[1]) + return f' -export_grad_fsl {ARGS.export_grad_fsl[0]} {ARGS.export_grad_fsl[1]}' return '' @@ -1398,14 +1499,14 @@ def handler(signum, _frame): for (key, value) in _SIGNALS.items(): try: if getattr(signal, key) == signum: - msg += key + ' (' + str(int(signum)) + ')] ' + value + msg += f'{key} ({int(signum)})] {value}' signal_found = True break except AttributeError: pass if not signal_found: msg += '?] Unknown system signal' - sys.stderr.write('\n' + EXEC_NAME + ': ' + ANSI.error + msg + ANSI.clear + '\n') + sys.stderr.write(f'\n{EXEC_NAME}: {ANSI.error}{msg}{ANSI.clear}\n') if os.getcwd() != WORKING_DIR: os.chdir(WORKING_DIR) if SCRATCH_DIR: @@ -1416,7 +1517,7 @@ def handler(signum, _frame): pass SCRATCH_DIR = '' else: - sys.stderr.write(EXEC_NAME + ': ' + ANSI.console + 'Scratch directory retained; location: ' + SCRATCH_DIR + ANSI.clear + '\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.console}Scratch directory retained; location: {SCRATCH_DIR}{ANSI.clear}\n') for item in _STDIN_IMAGES: try: os.remove(item) diff --git a/lib/mrtrix3/dwi2mask/3dautomask.py b/lib/mrtrix3/dwi2mask/3dautomask.py index a42b39ed22..c5be8d8f28 100644 --- a/lib/mrtrix3/dwi2mask/3dautomask.py +++ b/lib/mrtrix3/dwi2mask/3dautomask.py @@ -17,6 +17,7 @@ from mrtrix3 import MRtrixError from mrtrix3 import app, run +NEEDS_MEAN_BZERO = True # pylint: disable=unused-variable AFNI3DAUTOMASK_CMD = '3dAutomask' @@ -24,72 +25,100 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('3dautomask', parents=[base_parser]) parser.set_author('Ricardo Rios (ricardo.rios@cimat.mx)') parser.set_synopsis('Use AFNI 3dAutomask to derive a brain mask from the DWI mean b=0 image') - parser.add_citation('RW Cox. AFNI: Software for analysis and visualization of functional magnetic resonance neuroimages. Computers and Biomedical Research, 29:162-173, 1996.', is_external=True) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') - options = parser.add_argument_group('Options specific to the \'afni_3dautomask\' algorithm') - options.add_argument('-clfrac', type=app.Parser.Float(0.1, 0.9), help='Set the \'clip level fraction\', must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger.') - options.add_argument('-nograd', action='store_true', help='The program uses a \'gradual\' clip level by default. Add this option to use a fixed clip level.') - options.add_argument('-peels', type=app.Parser.Int(0), help='Peel (erode) the mask n times, then unpeel (dilate).') - options.add_argument('-nbhrs', type=app.Parser.Int(6, 26), help='Define the number of neighbors needed for a voxel NOT to be eroded. It should be between 6 and 26.') - options.add_argument('-eclip', action='store_true', help='After creating the mask, remove exterior voxels below the clip threshold.') - options.add_argument('-SI', type=app.Parser.Float(0.0), help='After creating the mask, find the most superior voxel, then zero out everything more than SI millimeters inferior to that. 130 seems to be decent (i.e., for Homo Sapiens brains).') - options.add_argument('-dilate', type=app.Parser.Int(0), help='Dilate the mask outwards n times') - options.add_argument('-erode', type=app.Parser.Int(0), help='Erode the mask outwards n times') - - options.add_argument('-NN1', action='store_true', help='Erode and dilate based on mask faces') - options.add_argument('-NN2', action='store_true', help='Erode and dilate based on mask edges') - options.add_argument('-NN3', action='store_true', help='Erode and dilate based on mask corners') - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return True + parser.add_citation('RW Cox. ' + 'AFNI: Software for analysis and visualization of functional magnetic resonance neuroimages. ' + 'Computers and Biomedical Research, 29:162-173, 1996.', + is_external=True) + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') + options = parser.add_argument_group('Options specific to the "3dautomask" algorithm') + options.add_argument('-clfrac', + type=app.Parser.Float(0.1, 0.9), + help='Set the "clip level fraction"; ' + 'must be a number between 0.1 and 0.9. ' + 'A small value means to make the initial threshold for clipping smaller, ' + 'which will tend to make the mask larger.') + options.add_argument('-nograd', + action='store_true', + help='The program uses a "gradual" clip level by default. ' + 'Add this option to use a fixed clip level.') + options.add_argument('-peels', + type=app.Parser.Int(0), + help='Peel (erode) the mask n times, ' + 'then unpeel (dilate).') + options.add_argument('-nbhrs', + type=app.Parser.Int(6, 26), + help='Define the number of neighbors needed for a voxel NOT to be eroded. ' + 'It should be between 6 and 26.') + options.add_argument('-eclip', + action='store_true', + help='After creating the mask, ' + 'remove exterior voxels below the clip threshold.') + options.add_argument('-SI', + type=app.Parser.Float(0.0), + help='After creating the mask, ' + 'find the most superior voxel, ' + 'then zero out everything more than SI millimeters inferior to that. ' + '130 seems to be decent (i.e., for Homo Sapiens brains).') + options.add_argument('-dilate', + type=app.Parser.Int(0), + help='Dilate the mask outwards n times') + options.add_argument('-erode', + type=app.Parser.Int(0), + help='Erode the mask outwards n times') + + options.add_argument('-NN1', + action='store_true', + help='Erode and dilate based on mask faces') + options.add_argument('-NN2', + action='store_true', + help='Erode and dilate based on mask edges') + options.add_argument('-NN3', + action='store_true', + help='Erode and dilate based on mask corners') def execute(): #pylint: disable=unused-variable if not shutil.which(AFNI3DAUTOMASK_CMD): - raise MRtrixError('Unable to locate AFNI "' - + AFNI3DAUTOMASK_CMD - + '" executable; check installation') + raise MRtrixError(f'Unable to locate AFNI "{AFNI3DAUTOMASK_CMD}" executable; ' + f'check installation') # main command to execute mask_path = 'afni_mask.nii.gz' - cmd_string = AFNI3DAUTOMASK_CMD + ' -prefix ' + mask_path + cmd_string = [AFNI3DAUTOMASK_CMD, '-prefix', mask_path] # Adding optional parameters if app.ARGS.clfrac is not None: - cmd_string += ' -clfrac ' + str(app.ARGS.clfrac) + cmd_string.extend(['-clfrac', str(app.ARGS.clfrac)]) if app.ARGS.peels is not None: - cmd_string += ' -peels ' + str(app.ARGS.peels) + cmd_string.extend(['-peels', str(app.ARGS.peels)]) if app.ARGS.nbhrs is not None: - cmd_string += ' -nbhrs ' + str(app.ARGS.nbhrs) + cmd_string.extend(['-nbhrs', str(app.ARGS.nbhrs)]) if app.ARGS.dilate is not None: - cmd_string += ' -dilate ' + str(app.ARGS.dilate) + cmd_string.extend(['-dilate', str(app.ARGS.dilate)]) if app.ARGS.erode is not None: - cmd_string += ' -erode ' + str(app.ARGS.erode) + cmd_string.extend(['-erode', str(app.ARGS.erode)]) if app.ARGS.SI is not None: - cmd_string += ' -SI ' + str(app.ARGS.SI) + cmd_string.extend(['-SI', str(app.ARGS.SI)]) if app.ARGS.nograd: - cmd_string += ' -nograd' + cmd_string.append('-nograd') if app.ARGS.eclip: - cmd_string += ' -eclip' + cmd_string.append('-eclip') if app.ARGS.NN1: - cmd_string += ' -NN1' + cmd_string.append('-NN1') if app.ARGS.NN2: - cmd_string += ' -NN2' + cmd_string.append('-NN2') if app.ARGS.NN3: - cmd_string += ' -NN3' + cmd_string.append('-NN3') # Adding input dataset to main command - cmd_string += ' bzero.nii' + cmd_string.append('bzero.nii') # Execute main command for afni 3dautomask run.command(cmd_string) diff --git a/lib/mrtrix3/dwi2mask/ants.py b/lib/mrtrix3/dwi2mask/ants.py index 64c1649682..8ddab6d30f 100644 --- a/lib/mrtrix3/dwi2mask/ants.py +++ b/lib/mrtrix3/dwi2mask/ants.py @@ -15,9 +15,10 @@ import os, shutil from mrtrix3 import CONFIG, MRtrixError -from mrtrix3 import app, path, run +from mrtrix3 import app, run +NEEDS_MEAN_BZERO = True # pylint: disable=unused-variable ANTS_BRAIN_EXTRACTION_CMD = 'antsBrainExtraction.sh' @@ -25,33 +26,23 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('ants', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use ANTs Brain Extraction to derive a DWI brain mask') - parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. A reproducible evaluation of ANTs similarity metric performance in brain image registration. NeuroImage, 2011, 54, 2033-2044', is_external=True) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') + parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. ' + 'A reproducible evaluation of ANTs similarity metric performance in brain image registration. ' + 'NeuroImage, 2011, 54, 2033-2044', + is_external=True) + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') options = parser.add_argument_group('Options specific to the "ants" algorithm') - options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image and corresponding mask for antsBrainExtraction.sh to use; the template image should be T2-weighted.') - - - -def get_inputs(): #pylint: disable=unused-variable - if app.ARGS.template: - run.command('mrconvert ' + app.ARGS.template[0] + ' ' + path.to_scratch('template_image.nii') - + ' -strides +1,+2,+3') - run.command('mrconvert ' + app.ARGS.template[1] + ' ' + path.to_scratch('template_mask.nii') - + ' -strides +1,+2,+3 -datatype uint8') - elif all(item in CONFIG for item in ['Dwi2maskTemplateImage', 'Dwi2maskTemplateMask']): - run.command('mrconvert ' + CONFIG['Dwi2maskTemplateImage'] + ' ' + path.to_scratch('template_image.nii') - + ' -strides +1,+2,+3') - run.command('mrconvert ' + CONFIG['Dwi2maskTemplateMask'] + ' ' + path.to_scratch('template_mask.nii') - + ' -strides +1,+2,+3 -datatype uint8') - else: - raise MRtrixError('No template image information available from ' - 'either command-line or MRtrix configuration file(s)') - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return True + options.add_argument('-template', + type=app.Parser.ImageIn(), + metavar=('TemplateImage', 'MaskImage'), + nargs=2, + help='Provide the template image and corresponding mask for antsBrainExtraction.sh to use; ' + 'the template image should be T2-weighted.') @@ -61,9 +52,22 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Environment variable ANTSPATH is not set; ' 'please appropriately confirure ANTs software') if not shutil.which(ANTS_BRAIN_EXTRACTION_CMD): - raise MRtrixError('Unable to find command "' - + ANTS_BRAIN_EXTRACTION_CMD - + '"; please check ANTs installation') + raise MRtrixError(f'Unable to find command "{ANTS_BRAIN_EXTRACTION_CMD}"; ' + f'please check ANTs installation') + + if app.ARGS.template: + run.command(['mrconvert', app.ARGS.template[0], 'template_image.nii', + '-strides', '+1,+2,+3']) + run.command(['mrconvert', app.ARGS.template[1], 'template_mask.nii', + '-strides', '+1,+2,+3', '-datatype', 'uint8']) + elif all(item in CONFIG for item in ['Dwi2maskTemplateImage', 'Dwi2maskTemplateMask']): + run.command(['mrconvert', CONFIG['Dwi2maskTemplateImage'], 'template_image.nii', + '-strides', '+1,+2,+3']) + run.command(['mrconvert', CONFIG['Dwi2maskTemplateMask'], 'template_mask.nii', + '-strides', '+1,+2,+3', '-datatype', 'uint8']) + else: + raise MRtrixError('No template image information available from ' + 'either command-line or MRtrix configuration file(s)') run.command(ANTS_BRAIN_EXTRACTION_CMD + ' -d 3' diff --git a/lib/mrtrix3/dwi2mask/b02template.py b/lib/mrtrix3/dwi2mask/b02template.py index 44c4942c16..b8a5da1f11 100644 --- a/lib/mrtrix3/dwi2mask/b02template.py +++ b/lib/mrtrix3/dwi2mask/b02template.py @@ -13,10 +13,11 @@ # # For more details, see http://www.mrtrix.org/. -import os, shutil +import os, pathlib, shutil from mrtrix3 import CONFIG, MRtrixError -from mrtrix3 import app, fsl, path, run +from mrtrix3 import app, fsl, run +NEEDS_MEAN_BZERO = True # pylint: disable=unused-variable SOFTWARES = ['antsfull', 'antsquick', 'fsl'] DEFAULT_SOFTWARE = 'antsquick' @@ -53,207 +54,198 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('b02template', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Register the mean b=0 image to a T2-weighted template to back-propagate a brain mask') - parser.add_description('This script currently assumes that the template image provided via the first input to the -template option ' - 'is T2-weighted, and can therefore be trivially registered to a mean b=0 image.') + parser.add_description('This script currently assumes that the template image provided via the first input to the -template option is T2-weighted, ' + 'and can therefore be trivially registered to a mean b=0 image.') parser.add_description('Command-line option -ants_options can be used with either the "antsquick" or "antsfull" software options. ' - 'In both cases, image dimensionality is assumed to be 3, and so this should be omitted from the user-specified options.' - 'The input can be either a string (encased in double-quotes if more than one option is specified), or a path to a text file containing the requested options. ' - 'In the case of the "antsfull" software option, one will require the names of the fixed and moving images that are provided to the antsRegistration command: these are "template_image.nii" and "bzero.nii" respectively.') - parser.add_citation('M. Jenkinson, C.F. Beckmann, T.E. Behrens, M.W. Woolrich, S.M. Smith. FSL. NeuroImage, 62:782-90, 2012', + 'In both cases, image dimensionality is assumed to be 3, ' + 'and so this should be omitted from the user-specified options.' + 'The input can be either a string ' + '(encased in double-quotes if more than one option is specified), ' + 'or a path to a text file containing the requested options. ' + 'In the case of the "antsfull" software option, ' + 'one will require the names of the fixed and moving images that are provided to the antsRegistration command: ' + 'these are "template_image.nii" and "bzero.nii" respectively.') + parser.add_citation('M. Jenkinson, C.F. Beckmann, T.E. Behrens, M.W. Woolrich, S.M. Smith. ' + 'FSL. ' + 'NeuroImage, 62:782-90, 2012', condition='If FSL software is used for registration', is_external=True) - parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. A reproducible evaluation of ANTs similarity metric performance in brain image registration. NeuroImage, 2011, 54, 2033-2044', + parser.add_citation('B. Avants, N.J. Tustison, G. Song, P.A. Cook, A. Klein, J.C. Jee. ' + 'A reproducible evaluation of ANTs similarity metric performance in brain image registration. ' + 'NeuroImage, 2011, 54, 2033-2044', condition='If ANTs software is used for registration', is_external=True) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') options = parser.add_argument_group('Options specific to the "template" algorithm') - options.add_argument('-software', choices=SOFTWARES, help='The software to use for template registration; options are: ' + ','.join(SOFTWARES) + '; default is ' + DEFAULT_SOFTWARE) - options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide the template image to which the input data will be registered, and the mask to be projected to the input image. The template image should be T2-weighted.') + options.add_argument('-software', + choices=SOFTWARES, + help='The software to use for template registration; ' + f'options are: {",".join(SOFTWARES)}; ' + f'default is {DEFAULT_SOFTWARE}') + options.add_argument('-template', + type=app.Parser.ImageIn(), + metavar=('TemplateImage', 'MaskImage'), + nargs=2, + help='Provide the template image to which the input data will be registered, ' + 'and the mask to be projected to the input image. ' + 'The template image should be T2-weighted.') ants_options = parser.add_argument_group('Options applicable when using the ANTs software for registration') - ants_options.add_argument('-ants_options', metavar='" ANTsOptions"', help='Provide options to be passed to the ANTs registration command (see Description)') + ants_options.add_argument('-ants_options', + metavar='" ANTsOptions"', + help='Provide options to be passed to the ANTs registration command ' + '(see Description)') fsl_options = parser.add_argument_group('Options applicable when using the FSL software for registration') - fsl_options.add_argument('-flirt_options', metavar='" FlirtOptions"', help='Command-line options to pass to the FSL flirt command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to flirt)') - fsl_options.add_argument('-fnirt_config', type=app.Parser.FileIn(), metavar='file', help='Specify a FNIRT configuration file for registration') - - - -def get_inputs(): #pylint: disable=unused-variable - - reg_software = app.ARGS.software if app.ARGS.software else CONFIG.get('Dwi2maskTemplateSoftware', DEFAULT_SOFTWARE) - if reg_software.startswith('ants'): - if not os.environ.get('ANTSPATH', ''): - raise MRtrixError('Environment variable ANTSPATH is not set; ' - 'please appropriately configure ANTs software') - if app.ARGS.ants_options: - if os.path.isfile(path.from_user(app.ARGS.ants_options, False)): - run.function(shutil.copyfile, path.from_user(app.ARGS.ants_options, False), path.to_scratch('ants_options.txt', False)) - elif reg_software == 'fsl': - fsl_path = os.environ.get('FSLDIR', '') - if not fsl_path: - raise MRtrixError('Environment variable FSLDIR is not set; ' - 'please run appropriate FSL configuration script') - if app.ARGS.fnirt_config: - fnirt_config = path.from_user(app.ARGS.fnirt_config, False) - if not os.path.isfile(fnirt_config): - raise MRtrixError('No file found at -fnirt_config location "' + fnirt_config + '"') - elif 'Dwi2maskTemplateFSLFnirtConfig' in CONFIG: - fnirt_config = CONFIG['Dwi2maskTemplateFSLFnirtConfig'] - if not os.path.isfile(fnirt_config): - raise MRtrixError('No file found at config file entry "Dwi2maskTemplateFSLFnirtConfig" location "' + fnirt_config + '"') + fsl_options.add_argument('-flirt_options', + metavar='" FlirtOptions"', + help='Command-line options to pass to the FSL flirt command ' + '(provide a string within quotation marks that contains at least one space, ' + 'even if only passing a single command-line option to flirt)') + fsl_options.add_argument('-fnirt_config', + type=app.Parser.FileIn(), + metavar='file', + help='Specify a FNIRT configuration file for registration') + + + +def execute_ants(mode): + assert mode in ('full', 'quick') + + if not os.environ.get('ANTSPATH', ''): + raise MRtrixError('Environment variable ANTSPATH is not set; ' + 'please appropriately configure ANTs software') + def check_ants_executable(cmdname): + if not shutil.which(cmdname): + raise MRtrixError(f'Unable to find ANTs command "{cmdname}"; ' + 'please check ANTs installation') + check_ants_executable(ANTS_REGISTERFULL_CMD if mode == 'full' else ANTS_REGISTERQUICK_CMD) + check_ants_executable(ANTS_TRANSFORM_CMD) + if app.ARGS.ants_options: + ants_options_as_path = app.UserPath(app.ARGS.ants_options) + if ants_options_as_path.is_file(): + run.function(shutil.copyfile, ants_options_as_path, 'ants_options.txt') + with open('ants_options.txt', 'r', encoding='utf-8') as ants_options_file: + ants_options = ants_options_file.readlines() + ants_options = ' '.join(line.lstrip().rstrip('\n \\') for line in ants_options if line.strip() and not line.lstrip()[0] == '#') else: - fnirt_config = None - if fnirt_config: - run.function(shutil.copyfile, fnirt_config, path.to_scratch('fnirt_config.cnf', False)) + ants_options = app.ARGS.ants_options else: - assert False + if mode == 'full': + ants_options = CONFIG.get('Dwi2maskTemplateANTsFullOptions', ANTS_REGISTERFULL_OPTIONS) + elif mode == 'quick': + ants_options = CONFIG.get('Dwi2maskTemplateANTsQuickOptions', ANTS_REGISTERQUICK_OPTIONS) - if app.ARGS.template: - run.command('mrconvert ' + app.ARGS.template[0] + ' ' + path.to_scratch('template_image.nii') - + ' -strides +1,+2,+3') - run.command('mrconvert ' + app.ARGS.template[1] + ' ' + path.to_scratch('template_mask.nii') - + ' -strides +1,+2,+3 -datatype uint8') - elif all(item in CONFIG for item in ['Dwi2maskTemplateImage', 'Dwi2maskTemplateMask']): - run.command('mrconvert ' + CONFIG['Dwi2maskTemplateImage'] + ' ' + path.to_scratch('template_image.nii') - + ' -strides +1,+2,+3') - run.command('mrconvert ' + CONFIG['Dwi2maskTemplateMask'] + ' ' + path.to_scratch('template_mask.nii') - + ' -strides +1,+2,+3 -datatype uint8') + if mode == 'full': + # Use ANTs SyN for registration to template + run.command(f'{ANTS_REGISTERFULL_CMD} --dimensionality 3 --output ANTS {ants_options}') + ants_options_split = ants_options.split() + nonlinear = any(i for i in range(0, len(ants_options_split)-1) + if ants_options_split[i] == '--transform' + and not any(item in ants_options_split[i+1] for item in ['Rigid', 'Affine', 'Translation'])) else: - raise MRtrixError('No template image information available from ' - 'either command-line or MRtrix configuration file(s)') - + # Use ANTs SyNQuick for registration to template + run.command(f'{ANTS_REGISTERQUICK_CMD} -d 3 -f template_image.nii -m bzero.nii -o ANTS {ants_options}') + ants_options_split = ants_options.split() + nonlinear = not [i for i in range(0, len(ants_options_split)-1) + if ants_options_split[i] == '-t' + and ants_options_split[i+1] in ['t', 'r', 'a']] + + transformed_path = 'transformed.nii' + # Note: Don't use nearest-neighbour interpolation; + # allow "partial volume fractions" in output, and threshold later + run.command(f'{ANTS_TRANSFORM_CMD} -d 3 -i template_mask.nii -o {transformed_path} -r bzero.nii -t [ANTS0GenericAffine.mat,1]' + + (' -t ANTS1InverseWarp.nii.gz' if nonlinear else '')) + return transformed_path + + + +def execute_fsl(): + fsl_path = os.environ.get('FSLDIR', '') + if not fsl_path: + raise MRtrixError('Environment variable FSLDIR is not set; ' + 'please run appropriate FSL configuration script') + flirt_cmd = fsl.exe_name('flirt') + fnirt_cmd = fsl.exe_name('fnirt') + invwarp_cmd = fsl.exe_name('invwarp') + applywarp_cmd = fsl.exe_name('applywarp') + + flirt_options = app.ARGS.flirt_options \ + if app.ARGS.flirt_options \ + else CONFIG.get('Dwi2maskTemplateFSLFlirtOptions', '-dof 12') + if app.ARGS.fnirt_config: + fnirt_config = app.ARGS.fnirt_config + elif 'Dwi2maskTemplateFSLFnirtConfig' in CONFIG: + fnirt_config = pathlib.Path(CONFIG['Dwi2maskTemplateFSLFnirtConfig']) + if not fnirt_config.is_file: + raise MRtrixError(f'No file found at config file entry "Dwi2maskTemplateFSLFnirtConfig" location {fnirt_config}') + else: + fnirt_config = None + if fnirt_config: + run.function(shutil.copyfile, fnirt_config, 'fnirt_config.cnf') + + # Initial affine registration to template + run.command(f'{flirt_cmd} -ref template_image.nii -in bzero.nii -omat bzero_to_template.mat {flirt_options}' + + (' -v' if app.VERBOSITY >= 3 else '')) + + # Produce dilated template mask image, so that registration is not + # too influenced by effects at the edge of the processing mask + run.command('maskfilter template_mask.nii dilate - -npass 3 | ' + 'mrconvert - template_mask_dilated.nii -datatype uint8') + + # Non-linear registration to template + if os.path.isfile('fnirt_config.cnf'): + fnirt_config_option = ' --config=fnirt_config' + else: + fnirt_config_option = '' + app.console('No config file provided for FSL fnirt; it will use its internal defaults') + run.command(f'{fnirt_cmd} {fnirt_config_option} --ref=template_image.nii --refmask=template_mask_dilated.nii' + f' --in=bzero.nii --aff=bzero_to_template.mat --cout=bzero_to_template.nii' + + (' --verbose' if app.VERBOSITY >= 3 else '')) + fnirt_output_path = fsl.find_image('bzero_to_template') + # Invert non-linear warp from subject->template to template->subject + run.command(f'{invwarp_cmd} --ref=bzero.nii --warp={fnirt_output_path} --out=template_to_bzero.nii') + invwarp_output_path = fsl.find_image('template_to_bzero') -def needs_mean_bzero(): #pylint: disable=unused-variable - return True + # Transform mask image from template to subject + # Note: Don't use nearest-neighbour interpolation; + # allow "partial volume fractions" in output, and threshold later + run.command(f'{applywarp_cmd} --ref=bzero.nii --in=template_mask.nii --warp={invwarp_output_path} --out=transformed.nii') + return fsl.find_image('transformed.nii') def execute(): #pylint: disable=unused-variable + reg_software = app.ARGS.software \ + if app.ARGS.software \ + else CONFIG.get('Dwi2maskTemplateSoftware', DEFAULT_SOFTWARE) - reg_software = app.ARGS.software if app.ARGS.software else CONFIG.get('Dwi2maskTemplateSoftware', DEFAULT_SOFTWARE) + if app.ARGS.template: + run.command(['mrconvert', app.ARGS.template[0], 'template_image.nii', + '-strides', '+1,+2,+3']) + run.command(['mrconvert', app.ARGS.template[1], 'template_mask.nii', + '-strides', '+1,+2,+3', '-datatype', 'uint8']) + elif all(item in CONFIG for item in ['Dwi2maskTemplateImage', 'Dwi2maskTemplateMask']): + run.command(['mrconvert', CONFIG['Dwi2maskTemplateImage'], 'template_image.nii', + '-strides', '+1,+2,+3']) + run.command(['mrconvert', CONFIG['Dwi2maskTemplateMask'], 'template_mask.nii', + '-strides', '+1,+2,+3', '-datatype', 'uint8']) + else: + raise MRtrixError('No template image information available from ' + 'either command-line or MRtrix configuration file(s)') if reg_software.startswith('ants'): - - def check_ants_executable(cmdname): - if not shutil.which(cmdname): - raise MRtrixError('Unable to find ANTs command "' + cmdname + '"; please check ANTs installation') - check_ants_executable(ANTS_REGISTERFULL_CMD if reg_software == 'antsfull' else ANTS_REGISTERQUICK_CMD) - check_ants_executable(ANTS_TRANSFORM_CMD) - - if app.ARGS.ants_options: - if os.path.isfile('ants_options.txt'): - with open('ants_options.txt', 'r', encoding='utf-8') as ants_options_file: - ants_options = ants_options_file.readlines() - ants_options = ' '.join(line.lstrip().rstrip('\n \\') for line in ants_options if line.strip() and not line.lstrip()[0] == '#') - else: - ants_options = app.ARGS.ants_options - else: - if reg_software == 'antsfull': - ants_options = CONFIG.get('Dwi2maskTemplateANTsFullOptions', ANTS_REGISTERFULL_OPTIONS) - elif reg_software == 'antsquick': - ants_options = CONFIG.get('Dwi2maskTemplateANTsQuickOptions', ANTS_REGISTERQUICK_OPTIONS) - - # Use ANTs SyN for registration to template - if reg_software == 'antsfull': - run.command(ANTS_REGISTERFULL_CMD - + ' --dimensionality 3' - + ' --output ANTS' - + ' ' - + ants_options) - ants_options_split = ants_options.split() - nonlinear = any(i for i in range(0, len(ants_options_split)-1) - if ants_options_split[i] == '--transform' - and not any(item in ants_options_split[i+1] for item in ['Rigid', 'Affine', 'Translation'])) - else: - # Use ANTs SyNQuick for registration to template - run.command(ANTS_REGISTERQUICK_CMD - + ' -d 3' - + ' -f template_image.nii' - + ' -m bzero.nii' - + ' -o ANTS' - + ' ' - + ants_options) - ants_options_split = ants_options.split() - nonlinear = not [i for i in range(0, len(ants_options_split)-1) - if ants_options_split[i] == '-t' - and ants_options_split[i+1] in ['t', 'r', 'a']] - - transformed_path = 'transformed.nii' - # Note: Don't use nearest-neighbour interpolation; - # allow "partial volume fractions" in output, and threshold later - run.command(ANTS_TRANSFORM_CMD - + ' -d 3' - + ' -i template_mask.nii' - + ' -o ' + transformed_path - + ' -r bzero.nii' - + ' -t [ANTS0GenericAffine.mat,1]' - + (' -t ANTS1InverseWarp.nii.gz' if nonlinear else '')) - + transformed_path = execute_ants('full' if reg_software == 'antsfull' else 'quick') elif reg_software == 'fsl': - - flirt_cmd = fsl.exe_name('flirt') - fnirt_cmd = fsl.exe_name('fnirt') - invwarp_cmd = fsl.exe_name('invwarp') - applywarp_cmd = fsl.exe_name('applywarp') - - flirt_options = app.ARGS.flirt_options \ - if app.ARGS.flirt_options \ - else CONFIG.get('Dwi2maskTemplateFSLFlirtOptions', '-dof 12') - - # Initial affine registration to template - run.command(flirt_cmd - + ' -ref template_image.nii' - + ' -in bzero.nii' - + ' -omat bzero_to_template.mat' - + ' ' - + flirt_options - + (' -v' if app.VERBOSITY >= 3 else '')) - - # Produce dilated template mask image, so that registration is not - # too influenced by effects at the edge of the processing mask - run.command('maskfilter template_mask.nii dilate - -npass 3 | ' - 'mrconvert - template_mask_dilated.nii -datatype uint8') - - # Non-linear registration to template - if os.path.isfile('fnirt_config.cnf'): - fnirt_config_option = ' --config=fnirt_config' - else: - fnirt_config_option = '' - app.console('No config file provided for FSL fnirt; it will use its internal defaults') - run.command(fnirt_cmd - + fnirt_config_option - + ' --ref=template_image.nii' - + ' --refmask=template_mask_dilated.nii' - + ' --in=bzero.nii' - + ' --aff=bzero_to_template.mat' - + ' --cout=bzero_to_template.nii' - + (' --verbose' if app.VERBOSITY >= 3 else '')) - fnirt_output_path = fsl.find_image('bzero_to_template') - - # Invert non-linear warp from subject->template to template->subject - run.command(invwarp_cmd - + ' --ref=bzero.nii' - + ' --warp=' + fnirt_output_path - + ' --out=template_to_bzero.nii') - invwarp_output_path = fsl.find_image('template_to_bzero') - - # Transform mask image from template to subject - # Note: Don't use nearest-neighbour interpolation; - # allow "partial volume fractions" in output, and threshold later - run.command(applywarp_cmd - + ' --ref=bzero.nii' - + ' --in=template_mask.nii' - + ' --warp=' + invwarp_output_path - + ' --out=transformed.nii') - transformed_path = fsl.find_image('transformed.nii') - + transformed_path = execute_fsl() else: assert False # Instead of neaerest-neighbour interpolation during transformation, # apply a threshold of 0.5 at this point - run.command('mrthreshold ' - + transformed_path - + ' mask.mif -abs 0.5') + run.command(['mrthreshold', transformed_path, 'mask.mif', '-abs', '0.5']) return 'mask.mif' diff --git a/lib/mrtrix3/dwi2mask/consensus.py b/lib/mrtrix3/dwi2mask/consensus.py index 5997117f61..bb8f7abe6b 100644 --- a/lib/mrtrix3/dwi2mask/consensus.py +++ b/lib/mrtrix3/dwi2mask/consensus.py @@ -13,53 +13,46 @@ # # For more details, see http://www.mrtrix.org/. -import os from mrtrix3 import CONFIG, MRtrixError -from mrtrix3 import algorithm, app, path, run +from mrtrix3 import algorithm, app, run +NEEDS_MEAN_BZERO = False # pylint: disable=unused-variable DEFAULT_THRESHOLD = 0.501 def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('consensus', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Generate a brain mask based on the consensus of all dwi2mask algorithms') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') options = parser.add_argument_group('Options specific to the "consensus" algorithm') - options.add_argument('-algorithms', nargs='+', help='Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised') - options.add_argument('-masks', type=app.Parser.ImageOut(), metavar='image', help='Export a 4D image containing the individual algorithm masks') - options.add_argument('-template', type=app.Parser.ImageIn(), metavar=('TemplateImage', 'MaskImage'), nargs=2, help='Provide a template image and corresponding mask for those algorithms requiring such') - options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), default=DEFAULT_THRESHOLD, help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: ' + str(DEFAULT_THRESHOLD) + ')') - - - -def get_inputs(): #pylint: disable=unused-variable - if app.ARGS.template: - run.command('mrconvert ' + app.ARGS.template[0] + ' ' + path.to_scratch('template_image.nii') - + ' -strides +1,+2,+3') - run.command('mrconvert ' + app.ARGS.template[1] + ' ' + path.to_scratch('template_mask.nii') - + ' -strides +1,+2,+3 -datatype uint8') - elif all(item in CONFIG for item in ['Dwi2maskTemplateImage', 'Dwi2maskTemplateMask']): - run.command('mrconvert ' + CONFIG['Dwi2maskTemplateImage'] + ' ' + path.to_scratch('template_image.nii') - + ' -strides +1,+2,+3') - run.command('mrconvert ' + CONFIG['Dwi2maskTemplateMask'] + ' ' + path.to_scratch('template_mask.nii') - + ' -strides +1,+2,+3 -datatype uint8') - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return False + options.add_argument('-algorithms', + type=str, + nargs='+', + help='Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised') + options.add_argument('-masks', + type=app.Parser.ImageOut(), + metavar='image', + help='Export a 4D image containing the individual algorithm masks') + options.add_argument('-template', + type=app.Parser.ImageIn(), + metavar=('TemplateImage', 'MaskImage'), + nargs=2, + help='Provide a template image and corresponding mask for those algorithms requiring such') + options.add_argument('-threshold', + type=app.Parser.Float(0.0, 1.0), + default=DEFAULT_THRESHOLD, + help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask ' + f'(default: {DEFAULT_THRESHOLD})') def execute(): #pylint: disable=unused-variable - if app.ARGS.threshold <= 0.0 or app.ARGS.threshold > 1.0: - raise MRtrixError('-threshold parameter value must lie between 0.0 and 1.0') - - if app.ARGS.masks: - app.check_output_path(path.from_user(app.ARGS.masks, False)) - algorithm_list = [item for item in algorithm.get_list() if item != 'consensus'] app.debug(str(algorithm_list)) @@ -71,10 +64,8 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Cannot provide "consensus" in list of dwi2mask algorithms to utilise') invalid_algs = [entry for entry in user_algorithms if entry not in algorithm_list] if invalid_algs: - raise MRtrixError('Requested dwi2mask algorithm' - + ('s' if len(invalid_algs) > 1 else '') - + ' not available: ' - + str(invalid_algs)) + raise MRtrixError(f'Requested dwi2mask {"algorithms" if len(invalid_algs) > 1 else "algorithm"} not available: ' + f'{invalid_algs}') algorithm_list = user_algorithms # For "template" algorithm, can run twice with two different softwares @@ -87,21 +78,31 @@ def execute(): #pylint: disable=unused-variable algorithm_list.append('b02template -software fsl') app.debug(str(algorithm_list)) - if any(any(item in alg for item in ('ants', 'b02template')) for alg in algorithm_list) \ - and not os.path.isfile('template_image.nii'): - raise MRtrixError('Cannot include within consensus algorithms that necessitate use of a template image ' + if any(any(item in alg for item in ('ants', 'b02template')) for alg in algorithm_list): + if app.ARGS.template: + run.command(['mrconvert', app.ARGS.template[0], 'template_image.nii', + '-strides', '+1,+2,+3']) + run.command(['mrconvert', app.ARGS.template[1], 'template_mask.nii', + '-strides', '+1,+2,+3', '-datatype', 'uint8']) + elif all(item in CONFIG for item in ['Dwi2maskTemplateImage', 'Dwi2maskTemplateMask']): + run.command(['mrconvert', CONFIG['Dwi2maskTemplateImage'], 'template_image.nii', + '-strides', '+1,+2,+3']) + run.command(['mrconvert', CONFIG['Dwi2maskTemplateMask'], 'template_mask.nii', + '-strides', '+1,+2,+3', '-datatype', 'uint8']) + else: + raise MRtrixError('Cannot include within consensus algorithms that necessitate use of a template image ' 'if no template image is provided via command-line or configuration file') mask_list = [] for alg in algorithm_list: alg_string = alg.replace(' -software ', '_') - mask_path = alg_string + '.mif' - cmd = 'dwi2mask ' + alg + ' input.mif ' + mask_path + mask_path = f'{alg_string}.mif' + cmd = f'dwi2mask {alg} input.mif {mask_path}' # Ideally this would be determined based on the presence of this option # in the command's help page if any(item in alg for item in ['ants', 'b02template']): cmd += ' -template template_image.nii template_mask.nii' - cmd += ' -scratch ' + app.SCRATCH_DIR + cmd += f' -scratch {app.SCRATCH_DIR}' if not app.DO_CLEANUP: cmd += ' -nocleanup' try: @@ -110,28 +111,29 @@ def execute(): #pylint: disable=unused-variable except run.MRtrixCmdError as e_dwi2mask: app.warn('"dwi2mask ' + alg + '" failed; will be omitted from consensus') app.debug(str(e_dwi2mask)) - with open('error_' + alg_string + '.txt', 'w', encoding='utf-8') as f_error: - f_error.write(str(e_dwi2mask)) + with open(f'error_{alg_string}.txt', 'w', encoding='utf-8') as f_error: + f_error.write(str(e_dwi2mask) + '\n') app.debug(str(mask_list)) if not mask_list: raise MRtrixError('No dwi2mask algorithms were successful; cannot generate mask') if len(mask_list) == 1: - app.warn('Only one dwi2mask algorithm was successful; output mask will be this result and not a "consensus"') + app.warn('Only one dwi2mask algorithm was successful; ' + 'output mask will be this result and not a "consensus"') if app.ARGS.masks: - run.command('mrconvert ' + mask_list[0] + ' ' + path.from_user(app.ARGS.masks), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', mask_list[0], app.ARGS.masks], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) return mask_list[0] final_mask = 'consensus.mif' - app.console('Computing consensus from ' + str(len(mask_list)) + ' of ' + str(len(algorithm_list)) + ' algorithms') + app.console(f'Computing consensus from {len(mask_list)} of {len(algorithm_list)} algorithms') run.command(['mrcat', mask_list, '-axis', '3', 'all_masks.mif']) - run.command('mrmath all_masks.mif mean - -axis 3 | ' - 'mrthreshold - -abs ' + str(app.ARGS.threshold) + ' -comparison ge ' + final_mask) + run.command(f'mrmath all_masks.mif mean - -axis 3 | ' + f'mrthreshold - -abs {app.ARGS.threshold} -comparison ge {final_mask}') if app.ARGS.masks: - run.command('mrconvert all_masks.mif ' + path.from_user(app.ARGS.masks), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'all_masks.mif', app.ARGS.masks], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) return final_mask diff --git a/lib/mrtrix3/dwi2mask/fslbet.py b/lib/mrtrix3/dwi2mask/fslbet.py index e439e71c54..6ab1f79058 100644 --- a/lib/mrtrix3/dwi2mask/fslbet.py +++ b/lib/mrtrix3/dwi2mask/fslbet.py @@ -17,37 +17,52 @@ from mrtrix3 import MRtrixError from mrtrix3 import app, fsl, image, run +NEEDS_MEAN_BZERO = True # pylint: disable=unused-variable def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('fslbet', parents=[base_parser]) - parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') + parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) ' + 'and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the FSL Brain Extraction Tool (bet) to generate a brain mask') - parser.add_citation('Smith, S. M. Fast robust automated brain extraction. Human Brain Mapping, 2002, 17, 143-155', is_external=True) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') - options = parser.add_argument_group('Options specific to the \'fslbet\' algorithm') - options.add_argument('-bet_f', type=app.Parser.Float(0.0, 1.0), help='Fractional intensity threshold (0->1); smaller values give larger brain outline estimates') - options.add_argument('-bet_g', type=app.Parser.Float(-1.0, 1.0), help='Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top') - options.add_argument('-bet_c', type=app.Parser.SequenceFloat(), metavar='i,j,k', help='Centre-of-gravity (voxels not mm) of initial mesh surface') - options.add_argument('-bet_r', type=app.Parser.Float(0.0), help='Head radius (mm not voxels); initial surface sphere is set to half of this') - options.add_argument('-rescale', action='store_true', help='Rescale voxel size provided to BET to 1mm isotropic; can improve results for rodent data') - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return True + parser.add_citation('Smith, S. M. ' + 'Fast robust automated brain extraction. ' + 'Human Brain Mapping, 2002, 17, 143-155', + is_external=True) + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') + options = parser.add_argument_group('Options specific to the "fslbet" algorithm') + options.add_argument('-bet_f', + type=app.Parser.Float(0.0, 1.0), + help='Fractional intensity threshold (0->1); ' + 'smaller values give larger brain outline estimates') + options.add_argument('-bet_g', + type=app.Parser.Float(-1.0, 1.0), + help='Vertical gradient in fractional intensity threshold (-1->1); ' + 'positive values give larger brain outline at bottom, smaller at top') + options.add_argument('-bet_c', + type=app.Parser.SequenceFloat(), + metavar='i,j,k', + help='Centre-of-gravity (voxels not mm) of initial mesh surface') + options.add_argument('-bet_r', + type=app.Parser.Float(0.0), + help='Head radius (mm not voxels); ' + 'initial surface sphere is set to half of this') + options.add_argument('-rescale', + action='store_true', + help='Rescale voxel size provided to BET to 1mm isotropic; ' + 'can improve results for rodent data') def execute(): #pylint: disable=unused-variable if not os.environ.get('FSLDIR', ''): - raise MRtrixError('Environment variable FSLDIR is not set; please run appropriate FSL configuration script') + raise MRtrixError('Environment variable FSLDIR is not set; ' + 'please run appropriate FSL configuration script') bet_cmd = fsl.exe_name('bet') # Starting brain masking using BET @@ -58,22 +73,22 @@ def execute(): #pylint: disable=unused-variable else: b0_image = 'bzero.nii' - cmd_string = bet_cmd + ' ' + b0_image + ' DWI_BET -R -m' + cmd_string = f'{bet_cmd} {b0_image} DWI_BET -R -m' if app.ARGS.bet_f is not None: - cmd_string += ' -f ' + str(app.ARGS.bet_f) + cmd_string += f' -f {app.ARGS.bet_f}' if app.ARGS.bet_g is not None: - cmd_string += ' -g ' + str(app.ARGS.bet_g) + cmd_string += f' -g {app.ARGS.bet_g}' if app.ARGS.bet_r is not None: - cmd_string += ' -r ' + str(app.ARGS.bet_r) + cmd_string += f' -r {app.ARGS.bet_r}' if app.ARGS.bet_c is not None: - cmd_string += ' -c ' + ' '.join(str(item) for item in app.ARGS.bet_c) + cmd_string += f' -c {" ".join(map(str, app.ARGS.bet_c))}' # Running BET command run.command(cmd_string) mask = fsl.find_image('DWI_BET_mask') if app.ARGS.rescale: - run.command('mrconvert ' + mask + ' mask_rescaled.nii -vox ' + ','.join(str(value) for value in vox)) + run.command(['mrconvert', mask, 'mask_rescaled.nii', '-vox', ','.join(map(str, vox))]) return 'mask_rescaled.nii' return mask diff --git a/lib/mrtrix3/dwi2mask/hdbet.py b/lib/mrtrix3/dwi2mask/hdbet.py index d25fdce53a..51c1897b1c 100644 --- a/lib/mrtrix3/dwi2mask/hdbet.py +++ b/lib/mrtrix3/dwi2mask/hdbet.py @@ -17,7 +17,7 @@ from mrtrix3 import MRtrixError from mrtrix3 import app, run - +NEEDS_MEAN_BZERO = True # pylint: disable=unused-variable OUTPUT_IMAGE_PATH = 'bzero_bet_mask.nii.gz' @@ -26,22 +26,20 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('hdbet', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use HD-BET to derive a brain mask from the DWI mean b=0 image') - parser.add_citation('Isensee F, Schell M, Tursunova I, Brugnara G, Bonekamp D, Neuberger U, Wick A, Schlemmer HP, Heiland S, Wick W, Bendszus M, Maier-Hein KH, Kickingereder P. Automated brain extraction of multi-sequence MRI using artificial neural networks. Hum Brain Mapp. 2019; 1-13. https://doi.org/10.1002/hbm.24750', is_external=True) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') - options = parser.add_argument_group('Options specific to the \'hdbet\' algorithm') - options.add_argument('-nogpu', action='store_true', help='Do not attempt to run on the GPU') - - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return True + parser.add_citation('Isensee F, Schell M, Tursunova I, Brugnara G, Bonekamp D, Neuberger U, Wick A, Schlemmer HP, Heiland S, Wick W, Bendszus M, Maier-Hein KH, Kickingereder P. ' + 'Automated brain extraction of multi-sequence MRI using artificial neural networks. ' + 'Hum Brain Mapp. 2019; 1-13. https://doi.org/10.1002/hbm.24750', + is_external=True) + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') + options = parser.add_argument_group('Options specific to the "hdbet" algorithm') + options.add_argument('-nogpu', + action='store_true', + help='Do not attempt to run on the GPU') @@ -67,6 +65,6 @@ def execute(): #pylint: disable=unused-variable raise gpu_header = ('===\nGPU\n===\n') cpu_header = ('===\nCPU\n===\n') - exception_stdout = gpu_header + e_gpu.stdout + '\n\n' + cpu_header + e_cpu.stdout + '\n\n' - exception_stderr = gpu_header + e_gpu.stderr + '\n\n' + cpu_header + e_cpu.stderr + '\n\n' + exception_stdout = f'{gpu_header}{e_gpu.stdout}\n\n{cpu_header}{e_cpu.stdout}\n\n' + exception_stderr = f'{gpu_header}{e_gpu.stderr}\n\n{cpu_header}{e_cpu.stderr}\n\n' raise run.MRtrixCmdError('hd-bet', 1, exception_stdout, exception_stderr) diff --git a/lib/mrtrix3/dwi2mask/legacy.py b/lib/mrtrix3/dwi2mask/legacy.py index 324d7230fe..002269fd82 100644 --- a/lib/mrtrix3/dwi2mask/legacy.py +++ b/lib/mrtrix3/dwi2mask/legacy.py @@ -15,32 +15,27 @@ from mrtrix3 import app, run +NEEDS_MEAN_BZERO = False # pylint: disable=unused-variable DEFAULT_CLEAN_SCALE = 2 - def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('legacy', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the legacy MRtrix3 dwi2mask heuristic (based on thresholded trace images)') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') parser.add_argument('-clean_scale', type=app.Parser.Int(0), default=DEFAULT_CLEAN_SCALE, - help='the maximum scale used to cut bridges. A certain maximum scale cuts ' - 'bridges up to a width (in voxels) of 2x the provided scale. Setting ' - 'this to 0 disables the mask cleaning step. (Default: ' + str(DEFAULT_CLEAN_SCALE) + ')') - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return False + help='the maximum scale used to cut bridges. ' + 'A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. ' + 'Setting this to 0 disables the mask cleaning step. ' + f'(Default: {DEFAULT_CLEAN_SCALE})') @@ -53,6 +48,6 @@ def execute(): #pylint: disable=unused-variable 'maskfilter - median - | ' 'maskfilter - connect -largest - | ' 'maskfilter - fill - | ' - 'maskfilter - clean -scale ' + str(app.ARGS.clean_scale) + ' mask.mif') + f'maskfilter - clean -scale {app.ARGS.clean_scale} mask.mif') return 'mask.mif' diff --git a/lib/mrtrix3/dwi2mask/mean.py b/lib/mrtrix3/dwi2mask/mean.py index da2342f82a..d73b65f9f2 100644 --- a/lib/mrtrix3/dwi2mask/mean.py +++ b/lib/mrtrix3/dwi2mask/mean.py @@ -15,44 +15,43 @@ from mrtrix3 import app, run +NEEDS_MEAN_BZERO = False # pylint: disable=unused-variable DEFAULT_CLEAN_SCALE = 2 def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mean', parents=[base_parser]) parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au)') parser.set_synopsis('Generate a mask based on simply averaging all volumes in the DWI series') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') - options = parser.add_argument_group('Options specific to the \'mean\' algorithm') - options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='Comma separated list of shells to be included in the volume averaging') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') + options = parser.add_argument_group('Options specific to the "mean" algorithm') + options.add_argument('-shells', + type=app.Parser.SequenceFloat(), + metavar='bvalues', + help='Comma separated list of shells to be included in the volume averaging') options.add_argument('-clean_scale', type=app.Parser.Int(0), default=DEFAULT_CLEAN_SCALE, - help='the maximum scale used to cut bridges. A certain maximum scale cuts ' - 'bridges up to a width (in voxels) of 2x the provided scale. Setting ' - 'this to 0 disables the mask cleaning step. (Default: ' + str(DEFAULT_CLEAN_SCALE) + ')') - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return False + help='the maximum scale used to cut bridges. ' + 'A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. ' + 'Setting this to 0 disables the mask cleaning step. ' + f'(Default: {DEFAULT_CLEAN_SCALE})') def execute(): #pylint: disable=unused-variable - run.command(('dwiextract input.mif - -shells ' + ','.join(str(f) for f in app.ARGS.shells) + ' | mrmath -' \ + run.command(('dwiextract input.mif - -shells ' + ','.join(map(str, app.ARGS.shells)) + ' | mrmath -' \ if app.ARGS.shells \ - else 'mrmath input.mif') - + ' mean - -axis 3 |' - + ' mrthreshold - - |' - + ' maskfilter - connect -largest - |' - + ' maskfilter - fill - |' - + ' maskfilter - clean -scale ' + str(app.ARGS.clean_scale) + ' mask.mif') + else 'mrmath input.mif') + + ' mean - -axis 3 |' + ' mrthreshold - - |' + ' maskfilter - connect -largest - |' + ' maskfilter - fill - |' + f' maskfilter - clean -scale {app.ARGS.clean_scale} mask.mif') return 'mask.mif' diff --git a/lib/mrtrix3/dwi2mask/mtnorm.py b/lib/mrtrix3/dwi2mask/mtnorm.py index a1f4c88301..e51825951e 100644 --- a/lib/mrtrix3/dwi2mask/mtnorm.py +++ b/lib/mrtrix3/dwi2mask/mtnorm.py @@ -15,9 +15,10 @@ import math from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run +NEEDS_MEAN_BZERO = False # pylint: disable=unused-variable LMAXES_MULTI = [4, 0, 0] LMAXES_SINGLE = [4, 0] THRESHOLD_DEFAULT = 0.5 @@ -26,54 +27,58 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mtnorm', parents=[base_parser]) - parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') + parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' + 'and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') parser.set_synopsis('Derives a DWI brain mask by calculating and then thresholding a sum-of-tissue-densities image') parser.add_description('This script attempts to identify brain voxels based on the total density of macroscopic ' - 'tissues as estimated through multi-tissue deconvolution. Following response function ' - 'estimation and multi-tissue deconvolution, the sum of tissue densities is thresholded at a ' - 'fixed value (default is ' + str(THRESHOLD_DEFAULT) + '), and subsequent mask image cleaning ' - 'operations are performed.') + 'tissues as estimated through multi-tissue deconvolution. ' + 'Following response function estimation and multi-tissue deconvolution, ' + 'the sum of tissue densities is thresholded at a fixed value ' + f'(default is {THRESHOLD_DEFAULT}), ' + 'and subsequent mask image cleaning operations are performed.') parser.add_description('The operation of this script is a subset of that performed by the script "dwibiasnormmask". ' - 'Many users may find that comprehensive solution preferable; this dwi2mask algorithm is nevertheless ' - 'provided to demonstrate specifically the mask estimation portion of that command.') + 'Many users may find that comprehensive solution preferable; ' + 'this dwi2mask algorithm is nevertheless provided to demonstrate specifically the mask estimation portion of that command.') parser.add_description('The ODFs estimated within this optimisation procedure are by default of lower maximal spherical harmonic ' - 'degree than what would be advised for analysis. This is done for computational efficiency. This ' - 'behaviour can be modified through the -lmax command-line option.') + 'degree than what would be advised for analysis. ' + 'This is done for computational efficiency. ' + 'This behaviour can be modified through the -lmax command-line option.') parser.add_citation('Jeurissen, B; Tournier, J-D; Dhollander, T; Connelly, A & Sijbers, J. ' 'Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. ' 'NeuroImage, 2014, 103, 411-426') parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-init_mask', type=app.Parser.ImageIn(), metavar='image', - help='Provide an initial brain mask, which will constrain the response function estimation ' + help='Provide an initial brain mask, ' + 'which will constrain the response function estimation ' '(if omitted, the default dwi2mask algorithm will be used)') options.add_argument('-lmax', type=app.Parser.SequenceInt(), metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' - 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') + f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' + f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data') options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), metavar='value', default=THRESHOLD_DEFAULT, - help='the threshold on the total tissue density sum image used to derive the brain mask; default is ' + str(THRESHOLD_DEFAULT)) - options.add_argument('-tissuesum', metavar='image', help='Export the tissue sum image that was used to generate the mask') - - - -def get_inputs(): #pylint: disable=unused-variable - if app.ARGS.init_mask: - run.command(['mrconvert', path.from_user(app.ARGS.init_mask, False), path.to_scratch('init_mask.mif', False), '-datatype', 'bit']) - + help='the threshold on the total tissue density sum image used to derive the brain mask; ' + f'default is {THRESHOLD_DEFAULT}') + options.add_argument('-tissuesum', + type=app.Parser.ImageOut(), + metavar='image', + help='Export the tissue sum image that was used to generate the mask') -def needs_mean_bzero(): #pylint: disable=unused-variable - return False def execute(): #pylint: disable=unused-variable @@ -81,16 +86,13 @@ def execute(): #pylint: disable=unused-variable # Verify user inputs lmax = None if app.ARGS.lmax: - try: - lmax = [int(i) for i in app.ARGS.lmax.split(',')] - except ValueError as exc: - raise MRtrixError('Values provided to -lmax option must be a comma-separated list of integers') from exc + lmax = app.ARGS.lmax if any(value < 0 or value % 2 for value in lmax): raise MRtrixError('lmax values must be non-negative even integers') if len(lmax) not in [2, 3]: raise MRtrixError('Length of lmax vector expected to be either 2 or 3') - if app.ARGS.threshold <= 0.0 or app.ARGS.threshold >= 1.0: - raise MRtrixError('Tissue density sum threshold must lie within the range (0.0, 1.0)') + if app.ARGS.init_mask: + run.command(['mrconvert', app.ARGS.init_mask, 'init_mask.mif', '-datatype', 'bit']) # Determine whether we are working with single-shell or multi-shell data bvalues = [ @@ -101,62 +103,47 @@ def execute(): #pylint: disable=unused-variable if lmax is None: lmax = LMAXES_MULTI if multishell else LMAXES_SINGLE elif len(lmax) == 3 and not multishell: - raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, but input DWI is not multi-shell') + raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, ' + 'but input DWI is not multi-shell') class Tissue(object): #pylint: disable=useless-object-inheritance def __init__(self, name): self.name = name - self.tissue_rf = 'response_' + name + '.txt' - self.fod = 'FOD_' + name + '.mif' + self.tissue_rf = f'response_{name}.txt' + self.fod = f'FOD_{name}.mif' dwi_image = 'input.mif' tissues = [Tissue('WM'), Tissue('GM'), Tissue('CSF')] - run.command('dwi2response dhollander ' - + dwi_image - + (' -mask init_mask.mif' if app.ARGS.init_mask else '') - + ' ' - + ' '.join(tissue.tissue_rf for tissue in tissues)) + run.command(['dwi2response', 'dhollander', dwi_image, [tissue.tissue_rf for tissue in tissues]] + + (['-mask', 'init_mask.mif'] if app.ARGS.init_mask else [])) # Immediately remove GM if we can't deal with it if not multishell: app.cleanup(tissues[1].tissue_rf) tissues = tissues[::2] - run.command('dwi2fod msmt_csd ' - + dwi_image - + ' -lmax ' + ','.join(str(item) for item in lmax) - + ' ' + run.command(f'dwi2fod msmt_csd {dwi_image} -lmax {",".join(map(str, lmax))} ' + ' '.join(tissue.tissue_rf + ' ' + tissue.fod for tissue in tissues)) tissue_sum_image = 'tissuesum.mif' - run.command('mrconvert ' - + tissues[0].fod - + ' -coord 3 0 - |' - + ' mrmath - ' - + ' '.join(tissue.fod for tissue in tissues[1:]) - + ' sum - | ' - + 'mrcalc - ' + str(math.sqrt(4.0 * math.pi)) + ' -mult ' - + tissue_sum_image) + run.command(f'mrconvert {tissues[0].fod} -coord 3 0 - | ' + f'mrmath - {" ".join(tissue.fod for tissue in tissues[1:])} sum - | ' + f'mrcalc - {math.sqrt(4.0 * math.pi)} -mult {tissue_sum_image}') mask_image = 'mask.mif' - run.command('mrthreshold ' - + tissue_sum_image - + ' -abs ' - + str(app.ARGS.threshold) - + ' - |' - + ' maskfilter - connect -largest - |' - + ' mrcalc 1 - -sub - -datatype bit |' - + ' maskfilter - connect -largest - |' - + ' mrcalc 1 - -sub - -datatype bit |' - + ' maskfilter - clean ' - + mask_image) + run.command(f'mrthreshold {tissue_sum_image} -abs {app.ARGS.threshold} - | ' + f'maskfilter - connect -largest - | ' + f'mrcalc 1 - -sub - -datatype bit | ' + f'maskfilter - connect -largest - | ' + f'mrcalc 1 - -sub - -datatype bit | ' + f'maskfilter - clean {mask_image}') app.cleanup([tissue.fod for tissue in tissues]) if app.ARGS.tissuesum: - run.command(['mrconvert', tissue_sum_image, path.from_user(app.ARGS.tissuesum, False)], - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', tissue_sum_image, app.ARGS.tissuesum], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) return mask_image diff --git a/lib/mrtrix3/dwi2mask/synthstrip.py b/lib/mrtrix3/dwi2mask/synthstrip.py index f8f2296587..5d6b29a56e 100644 --- a/lib/mrtrix3/dwi2mask/synthstrip.py +++ b/lib/mrtrix3/dwi2mask/synthstrip.py @@ -15,10 +15,10 @@ import shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, path, run - +from mrtrix3 import app, run +NEEDS_MEAN_BZERO = True # pylint: disable=unused-variable SYNTHSTRIP_CMD='mri_synthstrip' SYNTHSTRIP_SINGULARITY='sythstrip-singularity' @@ -28,26 +28,37 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Ruobing Chen (chrc@student.unimelb.edu.au)') parser.set_synopsis('Use the FreeSurfer Synthstrip method on the mean b=0 image') parser.add_description('This algorithm requires that the SynthStrip method be installed and available via PATH; ' - 'this could be via Freesufer 7.3.0 or later, or the dedicated Singularity container.') - parser.add_citation('A. Hoopes, J. S. Mora, A. V. Dalca, B. Fischl, M. Hoffmann. SynthStrip: Skull-Stripping for Any Brain Image. NeuroImage, 2022, 260, 119474', is_external=True) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') + 'this could be via Freesufer 7.3.0 or later, ' + 'or the dedicated Singularity container.') + parser.add_citation('A. Hoopes, J. S. Mora, A. V. Dalca, B. Fischl, M. Hoffmann. ' + 'SynthStrip: Skull-Stripping for Any Brain Image. ' + 'NeuroImage, 2022, 260, 119474', + is_external=True) + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') options=parser.add_argument_group('Options specific to the \'Synthstrip\' algorithm') - options.add_argument('-stripped', type=app.Parser.ImageOut(), help='The output stripped image') - options.add_argument('-gpu', action='store_true', default=False, help='Use the GPU') - options.add_argument('-model', type=app.Parser.FileIn(), metavar='file', help='Alternative model weights') - options.add_argument('-nocsf', action='store_true', default=False, help='Compute the immediate boundary of brain matter excluding surrounding CSF') - options.add_argument('-border', type=float, help='Control the boundary distance from the brain') - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return True + options.add_argument('-stripped', + type=app.Parser.ImageOut(), + help='The output stripped image') + options.add_argument('-gpu', + action='store_true', + default=False, + help='Use the GPU') + options.add_argument('-model', + type=app.Parser.FileIn(), + metavar='file', + help='Alternative model weights') + options.add_argument('-nocsf', + action='store_true', + default=False, + help='Compute the immediate boundary of brain matter excluding surrounding CSF') + options.add_argument('-border', + type=app.Parser.Int(), + help='Control the boundary distance from the brain') @@ -57,29 +68,30 @@ def execute(): #pylint: disable=unused-variable if not synthstrip_cmd: synthstrip_cmd=shutil.which(SYNTHSTRIP_SINGULARITY) if not synthstrip_cmd: - raise MRtrixError('Unable to locate "Synthstrip" executable; please check installation') + raise MRtrixError('Unable to locate "Synthstrip" executable; ' + 'please check installation') output_file = 'synthstrip_mask.nii' stripped_file = 'stripped.nii' - cmd_string = SYNTHSTRIP_CMD + ' -i bzero.nii -m ' + output_file + cmd = [SYNTHSTRIP_CMD, '-i', 'bzero.nii', '-m', output_file] if app.ARGS.stripped: - cmd_string += ' -o ' + stripped_file + cmd.extend(['-o', stripped_file]) if app.ARGS.gpu: - cmd_string += ' -g' + cmd.append('-g') if app.ARGS.nocsf: - cmd_string += ' --no-csf' + cmd.append('--no-csf') if app.ARGS.border is not None: - cmd_string += ' -b' + ' ' + str(app.ARGS.border) + cmd.extend(['-b', str(app.ARGS.border)]) if app.ARGS.model: - cmd_string += ' --model' + path.from_user(app.ARGS.model) + cmd.extend(['--model', app.ARGS.model]) - run.command(cmd_string) + run.command(cmd) if app.ARGS.stripped: - run.command('mrconvert ' + stripped_file + ' ' + path.from_user(app.ARGS.stripped), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', stripped_file, app.ARGS.stripped], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) return output_file diff --git a/lib/mrtrix3/dwi2mask/trace.py b/lib/mrtrix3/dwi2mask/trace.py index 99b38a050a..00b53d06d2 100644 --- a/lib/mrtrix3/dwi2mask/trace.py +++ b/lib/mrtrix3/dwi2mask/trace.py @@ -16,45 +16,50 @@ import math, os from mrtrix3 import app, image, run +NEEDS_MEAN_BZERO = False # pylint: disable=unused-variable DEFAULT_CLEAN_SCALE = 2 DEFAULT_MAX_ITERS = 10 def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('trace', parents=[base_parser]) - parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) and Robert E. Smith (robert.smith@florey.edu.au)') + parser.set_author('Warda Syeda (wtsyeda@unimelb.edu.au) ' + 'and Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('A method to generate a brain mask from trace images of b-value shells') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output mask image') - options = parser.add_argument_group('Options specific to the \'trace\' algorithm') - options.add_argument('-shells', type=app.Parser.SequenceFloat(), metavar='bvalues', help='Comma-separated list of shells used to generate trace-weighted images for masking') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output mask image') + options = parser.add_argument_group('Options specific to the "trace" algorithm') + options.add_argument('-shells', + type=app.Parser.SequenceFloat(), + metavar='bvalues', + help='Comma-separated list of shells used to generate trace-weighted images for masking') options.add_argument('-clean_scale', type=app.Parser.Int(0), default=DEFAULT_CLEAN_SCALE, - help='the maximum scale used to cut bridges. A certain maximum scale cuts ' - 'bridges up to a width (in voxels) of 2x the provided scale. Setting ' - 'this to 0 disables the mask cleaning step. (Default: ' + str(DEFAULT_CLEAN_SCALE) + ')') - iter_options = parser.add_argument_group('Options for turning \'dwi2mask trace\' into an iterative algorithm') + help='the maximum scale used to cut bridges. ' + 'A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. ' + 'Setting this to 0 disables the mask cleaning step. ' + f'(Default: {DEFAULT_CLEAN_SCALE})') + iter_options = parser.add_argument_group('Options for turning "dwi2mask trace" into an iterative algorithm') iter_options.add_argument('-iterative', action='store_true', help='(EXPERIMENTAL) Iteratively refine the weights for combination of per-shell trace-weighted images prior to thresholding') - iter_options.add_argument('-max_iters', type=app.Parser.Int(1), default=DEFAULT_MAX_ITERS, help='Set the maximum number of iterations for the algorithm (default: ' + str(DEFAULT_MAX_ITERS) + ')') - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_mean_bzero(): #pylint: disable=unused-variable - return False + iter_options.add_argument('-max_iters', + type=app.Parser.Int(1), + default=DEFAULT_MAX_ITERS, + help='Set the maximum number of iterations for the algorithm ' + f'(default: {DEFAULT_MAX_ITERS})') def execute(): #pylint: disable=unused-variable if app.ARGS.shells: - run.command('dwiextract input.mif input_shells.mif -shells ' + ','.join(str(item) for item in app.ARGS.shells)) + run.command('dwiextract input.mif input_shells.mif ' + f'-shells {",".join(map(str, app.ARGS.shells))}') run.command('dwishellmath input_shells.mif mean shell_traces.mif') else: run.command('dwishellmath input.mif mean shell_traces.mif') @@ -66,9 +71,9 @@ def execute(): #pylint: disable=unused-variable shell_count = image.Header('shell_traces.mif').size()[-1] progress = app.ProgressBar('Performing per-shell histogram matching', shell_count-1) for index in range(1, shell_count): - filename = 'shell-{:02d}.mif'.format(index) - run.command('mrconvert shell_traces.mif -coord 3 ' + str(index) + ' -axes 0,1,2 - | ' - 'mrhistmatch scale - shell-00.mif ' + filename) + filename = f'shell-{index:02d}.mif' + run.command(f'mrconvert shell_traces.mif -coord 3 {index} -axes 0,1,2 - | ' + f'mrhistmatch scale - shell-00.mif {filename}') files.append(filename) progress.increment() progress.done() @@ -96,29 +101,29 @@ def execute(): #pylint: disable=unused-variable current_mask = 'init_mask.mif' iteration = 0 while True: - current_mask_inv = os.path.splitext(current_mask)[0] + '_inv.mif' - run.command('mrcalc 1 ' + current_mask + ' -sub ' + current_mask_inv + ' -datatype bit') + current_mask_inv = f'{os.path.splitext(current_mask)[0]}_inv.mif' + run.command(f'mrcalc 1 {current_mask} -sub {current_mask_inv} -datatype bit') shell_weights = [] iteration += 1 for index in range(0, shell_count): - stats_inside = image.statistics('shell-{:02d}.mif'.format(index), mask=current_mask) - stats_outside = image.statistics('shell-{:02d}.mif'.format(index), mask=current_mask_inv) + stats_inside = image.statistics(f'shell-{index:02d}.mif', mask=current_mask) + stats_outside = image.statistics(f'shell-{index:02d}.mif', mask=current_mask_inv) variance = (((stats_inside.count - 1) * stats_inside.std * stats_inside.std) \ + ((stats_outside.count - 1) * stats_outside.std * stats_outside.std)) \ / (stats_inside.count + stats_outside.count - 2) cohen_d = (stats_inside.mean - stats_outside.mean) / math.sqrt(variance) shell_weights.append(cohen_d) - mask_path = 'mask-{:02d}.mif'.format(iteration) - run.command('mrcalc shell-00.mif ' + str(shell_weights[0]) + ' -mult ' - + ' -add '.join(filepath + ' ' + str(weight) + ' -mult' for filepath, weight in zip(files[1:], shell_weights[1:])) - + ' -add - |' - + ' mrthreshold - - |' - + ' maskfilter - connect -largest - |' - + ' maskfilter - fill - |' - + ' maskfilter - clean -scale ' + str(app.ARGS.clean_scale) + ' - |' - + ' mrcalc input_pos_mask.mif - -mult ' + mask_path + ' -datatype bit') - mask_mismatch_path = 'mask_mismatch-{:02d}.mif'.format(iteration) - run.command('mrcalc ' + current_mask + ' ' + mask_path + ' -sub -abs ' + mask_mismatch_path) + mask_path = f'mask-{iteration:02d}.mif' + run.command(f'mrcalc shell-00.mif {shell_weights[0]} -mult ' + + ' -add '.join(filepath + ' ' + str(weight) + ' -mult' for filepath, weight in zip(files[1:], shell_weights[1:])) + + ' -add - |' + ' mrthreshold - - |' + ' maskfilter - connect -largest - |' + ' maskfilter - fill - |' + f' maskfilter - clean -scale {app.ARGS.clean_scale} - |' + f' mrcalc input_pos_mask.mif - -mult {mask_path} -datatype bit') + mask_mismatch_path = f'mask_mismatch-{iteration:02d}.mif' + run.command(f'mrcalc {current_mask} {mask_path} -sub -abs {mask_mismatch_path}') if not image.statistics(mask_mismatch_path).mean: app.console('Terminating optimisation due to convergence of masks between iterations') return mask_path diff --git a/lib/mrtrix3/dwi2response/dhollander.py b/lib/mrtrix3/dwi2response/dhollander.py index 135e4b6ef8..3294eb8863 100644 --- a/lib/mrtrix3/dwi2response/dhollander.py +++ b/lib/mrtrix3/dwi2response/dhollander.py @@ -16,9 +16,12 @@ import math, shlex, shutil from mrtrix3 import CONFIG, MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run +NEEDS_SINGLE_SHELL = False # pylint: disable=unused-variable +SUPPORTS_MASK = True # pylint: disable=unused-variable + WM_ALGOS = [ 'fa', 'tax', 'tournier' ] @@ -26,44 +29,72 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('dhollander', parents=[base_parser]) parser.set_author('Thijs Dhollander (thijs.dhollander@gmail.com)') - parser.set_synopsis('Unsupervised estimation of WM, GM and CSF response functions that does not require a T1 image (or segmentation thereof)') - parser.add_description('This is an improved version of the Dhollander et al. (2016) algorithm for unsupervised estimation of WM, GM and CSF response functions, which includes the Dhollander et al. (2019) improvements for single-fibre WM response function estimation (prior to this update, the "dwi2response tournier" algorithm had been utilised specifically for the single-fibre WM response function estimation step).') - parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') - parser.add_citation('Dhollander, T.; Mito, R.; Raffelt, D. & Connelly, A. Improved white matter response function estimation for 3-tissue constrained spherical deconvolution. Proc Intl Soc Mag Reson Med, 2019, 555', + parser.set_synopsis('Unsupervised estimation of WM, GM and CSF response functions ' + 'that does not require a T1 image (or segmentation thereof)') + parser.add_description('This is an improved version of the Dhollander et al. (2016) algorithm for unsupervised estimation of WM, GM and CSF response functions, ' + 'which includes the Dhollander et al. (2019) improvements for single-fibre WM response function estimation ' + '(prior to this update, ' + 'the "dwi2response tournier" algorithm had been utilised specifically for the single-fibre WM response function estimation step).') + parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' + 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' + 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') + parser.add_citation('Dhollander, T.; Mito, R.; Raffelt, D. & Connelly, A. ' + 'Improved white matter response function estimation for 3-tissue constrained spherical deconvolution. ' + 'Proc Intl Soc Mag Reson Med, 2019, 555', condition='If -wm_algo option is not used') - parser.add_argument('input', type=app.Parser.ImageIn(), help='Input DWI dataset') - parser.add_argument('out_sfwm', type=app.Parser.FileOut(), help='Output single-fibre WM response function text file') - parser.add_argument('out_gm', type=app.Parser.FileOut(), help='Output GM response function text file') - parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response function text file') - options = parser.add_argument_group('Options for the \'dhollander\' algorithm') - options.add_argument('-erode', type=app.Parser.Int(0), metavar='passes', default=3, help='Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3)') - options.add_argument('-fa', type=app.Parser.Float(0.0, 1.0), metavar='threshold', default=0.2, help='FA threshold for crude WM versus GM-CSF separation. (default: 0.2)') - options.add_argument('-sfwm', type=app.Parser.Float(0.0, 100.0), metavar='percentage', default=0.5, help='Final number of single-fibre WM voxels to select, as a percentage of refined WM. (default: 0.5 per cent)') - options.add_argument('-gm', type=app.Parser.Float(0.0, 100.0), metavar='percentage', default=2.0, help='Final number of GM voxels to select, as a percentage of refined GM. (default: 2 per cent)') - options.add_argument('-csf', type=app.Parser.Float(0.0, 100.0), metavar='percentage', default=10.0, help='Final number of CSF voxels to select, as a percentage of refined CSF. (default: 10 per cent)') - options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, help='Use external dwi2response algorithm for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + ') (default: built-in Dhollander 2019)') - - - -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.out_sfwm) - app.check_output_path(app.ARGS.out_gm) - app.check_output_path(app.ARGS.out_csf) - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_single_shell(): #pylint: disable=unused-variable - return False - - - -def supports_mask(): #pylint: disable=unused-variable - return True + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='Input DWI dataset') + parser.add_argument('out_sfwm', + type=app.Parser.FileOut(), + help='Output single-fibre WM response function text file') + parser.add_argument('out_gm', + type=app.Parser.FileOut(), + help='Output GM response function text file') + parser.add_argument('out_csf', + type=app.Parser.FileOut(), + help='Output CSF response function text file') + options = parser.add_argument_group('Options for the "dhollander" algorithm') + options.add_argument('-erode', + type=app.Parser.Int(0), + metavar='passes', + default=3, + help='Number of erosion passes to apply to initial (whole brain) mask. ' + 'Set to 0 to not erode the brain mask. ' + '(default: 3)') + options.add_argument('-fa', + type=app.Parser.Float(0.0, 1.0), + metavar='threshold', + default=0.2, + help='FA threshold for crude WM versus GM-CSF separation. ' + '(default: 0.2)') + options.add_argument('-sfwm', + type=app.Parser.Float(0.0, 100.0), + metavar='percentage', + default=0.5, + help='Final number of single-fibre WM voxels to select, ' + 'as a percentage of refined WM. ' + '(default: 0.5 per cent)') + options.add_argument('-gm', + type=app.Parser.Float(0.0, 100.0), + metavar='percentage', + default=2.0, + help='Final number of GM voxels to select, ' + 'as a percentage of refined GM. ' + '(default: 2 per cent)') + options.add_argument('-csf', + type=app.Parser.Float(0.0, 100.0), + metavar='percentage', + default=10.0, + help='Final number of CSF voxels to select, ' + 'as a percentage of refined CSF. ' + '(default: 10 per cent)') + options.add_argument('-wm_algo', + metavar='algorithm', + choices=WM_ALGOS, + help='Use external dwi2response algorithm for WM single-fibre voxel selection ' + f'(options: {", ".join(WM_ALGOS)}) ' + '(default: built-in Dhollander 2019)') @@ -77,10 +108,11 @@ def execute(): #pylint: disable=unused-variable # Get b-values and number of volumes per b-value. bvalues = [ int(round(float(x))) for x in image.mrinfo('dwi.mif', 'shell_bvalues').split() ] bvolumes = [ int(x) for x in image.mrinfo('dwi.mif', 'shell_sizes').split() ] - app.console(str(len(bvalues)) + ' unique b-value(s) detected: ' + ','.join(map(str,bvalues)) + ' with ' + ','.join(map(str,bvolumes)) + ' volumes') + app.console(f'{len(bvalues)} unique b-value(s) detected: ' + f'{",".join(map(str,bvalues))} with {",".join(map(str,bvolumes))} volumes') if len(bvalues) < 2: raise MRtrixError('Need at least 2 unique b-values (including b=0).') - bvalues_option = ' -shells ' + ','.join(map(str,bvalues)) + bvalues_option = f' -shells {",".join(map(str,bvalues))}' # Get lmax information (if provided). sfwm_lmax_option = '' @@ -88,7 +120,8 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.lmax: sfwm_lmax = app.ARGS.lmax if len(sfwm_lmax) != len(bvalues): - raise MRtrixError('Number of lmax\'s (' + str(len(sfwm_lmax)) + ', as supplied to the -lmax option: ' + ','.join(map(str,sfwm_lmax)) + ') does not match number of unique b-values.') + raise MRtrixError(f'Number of lmax\'s ({len(sfwm_lmax)}, as supplied to the -lmax option: ' + f'{",".join(map(str,sfwm_lmax))}) does not match number of unique b-values.') if any(sfl%2 for sfl in sfwm_lmax): raise MRtrixError('Values supplied to the -lmax option must be even.') if any(sfl<0 for sfl in sfwm_lmax): @@ -103,36 +136,39 @@ def execute(): #pylint: disable=unused-variable # Erode (brain) mask. if app.ARGS.erode > 0: app.console('* Eroding brain mask by ' + str(app.ARGS.erode) + ' pass(es)...') - run.command('maskfilter mask.mif erode eroded_mask.mif -npass ' + str(app.ARGS.erode), show=False) + run.command(f'maskfilter mask.mif erode eroded_mask.mif -npass {app.ARGS.erode}', show=False) else: app.console('Not eroding brain mask.') run.command('mrconvert mask.mif eroded_mask.mif -datatype bit', show=False) statmaskcount = image.statistics('mask.mif', mask='mask.mif').count statemaskcount = image.statistics('eroded_mask.mif', mask='eroded_mask.mif').count - app.console(' [ mask: ' + str(statmaskcount) + ' -> ' + str(statemaskcount) + ' ]') + app.console(f' [ mask: {statmaskcount} -> {statemaskcount} ]') # Get volumes, compute mean signal and SDM per b-value; compute overall SDM; get rid of erroneous values. app.console('* Computing signal decay metric (SDM):') totvolumes = 0 fullsdmcmd = 'mrcalc' errcmd = 'mrcalc' - zeropath = 'mean_b' + str(bvalues[0]) + '.mif' + zeropath = f'mean_b{bvalues[0]}.mif' for ibv, bval in enumerate(bvalues): - app.console(' * b=' + str(bval) + '...') - meanpath = 'mean_b' + str(bval) + '.mif' - run.command('dwiextract dwi.mif -shells ' + str(bval) + ' - | mrcalc - 0 -max - | mrmath - mean ' + meanpath + ' -axis 3', show=False) - errpath = 'err_b' + str(bval) + '.mif' - run.command('mrcalc ' + meanpath + ' -finite ' + meanpath + ' 0 -if 0 -le ' + errpath + ' -datatype bit', show=False) - errcmd += ' ' + errpath + app.console(f' * b={bval}...') + meanpath = f'mean_b{bval}.mif' + run.command(f'dwiextract dwi.mif -shells {bval} - | ' + f'mrcalc - 0 -max - | ' + f'mrmath - mean {meanpath} -axis 3', + show=False) + errpath = f'err_b{bval}.mif' + run.command(f'mrcalc {meanpath} -finite {meanpath} 0 -if 0 -le {errpath} -datatype bit', show=False) + errcmd += f' {errpath}' if ibv>0: errcmd += ' -add' - sdmpath = 'sdm_b' + str(bval) + '.mif' - run.command('mrcalc ' + zeropath + ' ' + meanpath + ' -divide -log ' + sdmpath, show=False) + sdmpath = f'sdm_b{bval}.mif' + run.command(f'mrcalc {zeropath} {meanpath} -divide -log {sdmpath}', show=False) totvolumes += bvolumes[ibv] - fullsdmcmd += ' ' + sdmpath + ' ' + str(bvolumes[ibv]) + ' -mult' + fullsdmcmd += f' {sdmpath} {bvolumes[ibv]} -mult' if ibv>1: fullsdmcmd += ' -add' - fullsdmcmd += ' ' + str(totvolumes) + ' -divide full_sdm.mif' + fullsdmcmd += f' {totvolumes} -divide full_sdm.mif' run.command(fullsdmcmd, show=False) app.console('* Removing erroneous voxels from mask and correcting SDM...') run.command('mrcalc full_sdm.mif -finite full_sdm.mif 0 -if 0 -le err_sdm.mif -datatype bit', show=False) @@ -140,7 +176,7 @@ def execute(): #pylint: disable=unused-variable run.command(errcmd, show=False) run.command('mrcalc safe_mask.mif full_sdm.mif 0 -if 10 -min safe_sdm.mif', show=False) statsmaskcount = image.statistics('safe_mask.mif', mask='safe_mask.mif').count - app.console(' [ mask: ' + str(statemaskcount) + ' -> ' + str(statsmaskcount) + ' ]') + app.console(f' [ mask: {statemaskcount} -> {statsmaskcount} ]') # CRUDE SEGMENTATION @@ -149,21 +185,26 @@ def execute(): #pylint: disable=unused-variable # Compute FA and principal eigenvectors; crude WM versus GM-CSF separation based on FA. app.console('* Crude WM versus GM-CSF separation (at FA=' + str(app.ARGS.fa) + ')...') - run.command('dwi2tensor dwi.mif - -mask safe_mask.mif | tensor2metric - -fa safe_fa.mif -vector safe_vecs.mif -modulate none -mask safe_mask.mif', show=False) - run.command('mrcalc safe_mask.mif safe_fa.mif 0 -if ' + str(app.ARGS.fa) + ' -gt crude_wm.mif -datatype bit', show=False) + run.command('dwi2tensor dwi.mif - -mask safe_mask.mif | ' + 'tensor2metric - -fa safe_fa.mif -vector safe_vecs.mif -modulate none -mask safe_mask.mif', + show=False) + run.command(f'mrcalc safe_mask.mif safe_fa.mif 0 -if {app.ARGS.fa} -gt crude_wm.mif -datatype bit', show=False) run.command('mrcalc crude_wm.mif 0 safe_mask.mif -if _crudenonwm.mif -datatype bit', show=False) statcrudewmcount = image.statistics('crude_wm.mif', mask='crude_wm.mif').count statcrudenonwmcount = image.statistics('_crudenonwm.mif', mask='_crudenonwm.mif').count - app.console(' [ ' + str(statsmaskcount) + ' -> ' + str(statcrudewmcount) + ' (WM) & ' + str(statcrudenonwmcount) + ' (GM-CSF) ]') + app.console(f' [ {statsmaskcount} -> {statcrudewmcount} (WM) & {statcrudenonwmcount} (GM-CSF) ]') # Crude GM versus CSF separation based on SDM. app.console('* Crude GM versus CSF separation...') crudenonwmmedian = image.statistics('safe_sdm.mif', mask='_crudenonwm.mif').median - run.command('mrcalc _crudenonwm.mif safe_sdm.mif ' + str(crudenonwmmedian) + ' -subtract 0 -if - | mrthreshold - - -mask _crudenonwm.mif | mrcalc _crudenonwm.mif - 0 -if crude_csf.mif -datatype bit', show=False) + run.command(f'mrcalc _crudenonwm.mif safe_sdm.mif {crudenonwmmedian} -subtract 0 -if - | ' + 'mrthreshold - - -mask _crudenonwm.mif | ' + 'mrcalc _crudenonwm.mif - 0 -if crude_csf.mif -datatype bit', + show=False) run.command('mrcalc crude_csf.mif 0 _crudenonwm.mif -if crude_gm.mif -datatype bit', show=False) statcrudegmcount = image.statistics('crude_gm.mif', mask='crude_gm.mif').count statcrudecsfcount = image.statistics('crude_csf.mif', mask='crude_csf.mif').count - app.console(' [ ' + str(statcrudenonwmcount) + ' -> ' + str(statcrudegmcount) + ' (GM) & ' + str(statcrudecsfcount) + ' (CSF) ]') + app.console(f' [ {statcrudenonwmcount} -> {statcrudegmcount} (GM) & {statcrudecsfcount} (CSF) ]') # REFINED SEGMENTATION @@ -173,32 +214,40 @@ def execute(): #pylint: disable=unused-variable # Refine WM: remove high SDM outliers. app.console('* Refining WM...') crudewmmedian = image.statistics('safe_sdm.mif', mask='crude_wm.mif').median - run.command('mrcalc crude_wm.mif safe_sdm.mif ' + str(crudewmmedian) + ' -subtract -abs 0 -if _crudewm_sdmad.mif', show=False) + run.command(f'mrcalc crude_wm.mif safe_sdm.mif {crudewmmedian} -subtract -abs 0 -if _crudewm_sdmad.mif', show=False) crudewmmad = image.statistics('_crudewm_sdmad.mif', mask='crude_wm.mif').median crudewmoutlthresh = crudewmmedian + (1.4826 * crudewmmad * 2.0) - run.command('mrcalc crude_wm.mif safe_sdm.mif 0 -if ' + str(crudewmoutlthresh) + ' -gt _crudewmoutliers.mif -datatype bit', show=False) + run.command(f'mrcalc crude_wm.mif safe_sdm.mif 0 -if {crudewmoutlthresh} -gt _crudewmoutliers.mif -datatype bit', show=False) run.command('mrcalc _crudewmoutliers.mif 0 crude_wm.mif -if refined_wm.mif -datatype bit', show=False) statrefwmcount = image.statistics('refined_wm.mif', mask='refined_wm.mif').count - app.console(' [ WM: ' + str(statcrudewmcount) + ' -> ' + str(statrefwmcount) + ' ]') + app.console(f' [ WM: {statcrudewmcount} -> {statrefwmcount} ]') # Refine GM: separate safer GM from partial volumed voxels. app.console('* Refining GM...') crudegmmedian = image.statistics('safe_sdm.mif', mask='crude_gm.mif').median - run.command('mrcalc crude_gm.mif safe_sdm.mif 0 -if ' + str(crudegmmedian) + ' -gt _crudegmhigh.mif -datatype bit', show=False) + run.command(f'mrcalc crude_gm.mif safe_sdm.mif 0 -if {crudegmmedian} -gt _crudegmhigh.mif -datatype bit', show=False) run.command('mrcalc _crudegmhigh.mif 0 crude_gm.mif -if _crudegmlow.mif -datatype bit', show=False) - run.command('mrcalc _crudegmhigh.mif safe_sdm.mif ' + str(crudegmmedian) + ' -subtract 0 -if - | mrthreshold - - -mask _crudegmhigh.mif -invert | mrcalc _crudegmhigh.mif - 0 -if _crudegmhighselect.mif -datatype bit', show=False) - run.command('mrcalc _crudegmlow.mif safe_sdm.mif ' + str(crudegmmedian) + ' -subtract -neg 0 -if - | mrthreshold - - -mask _crudegmlow.mif -invert | mrcalc _crudegmlow.mif - 0 -if _crudegmlowselect.mif -datatype bit', show=False) + run.command(f'mrcalc _crudegmhigh.mif safe_sdm.mif {crudegmmedian} -subtract 0 -if - | ' + 'mrthreshold - - -mask _crudegmhigh.mif -invert | ' + 'mrcalc _crudegmhigh.mif - 0 -if _crudegmhighselect.mif -datatype bit', + show=False) + run.command(f'mrcalc _crudegmlow.mif safe_sdm.mif {crudegmmedian} -subtract -neg 0 -if - | ' + 'mrthreshold - - -mask _crudegmlow.mif -invert | ' + 'mrcalc _crudegmlow.mif - 0 -if _crudegmlowselect.mif -datatype bit', show=False) run.command('mrcalc _crudegmhighselect.mif 1 _crudegmlowselect.mif -if refined_gm.mif -datatype bit', show=False) statrefgmcount = image.statistics('refined_gm.mif', mask='refined_gm.mif').count - app.console(' [ GM: ' + str(statcrudegmcount) + ' -> ' + str(statrefgmcount) + ' ]') + app.console(f' [ GM: {statcrudegmcount} -> {statrefgmcount} ]') # Refine CSF: recover lost CSF from crude WM SDM outliers, separate safer CSF from partial volumed voxels. app.console('* Refining CSF...') crudecsfmin = image.statistics('safe_sdm.mif', mask='crude_csf.mif').min - run.command('mrcalc _crudewmoutliers.mif safe_sdm.mif 0 -if ' + str(crudecsfmin) + ' -gt 1 crude_csf.mif -if _crudecsfextra.mif -datatype bit', show=False) - run.command('mrcalc _crudecsfextra.mif safe_sdm.mif ' + str(crudecsfmin) + ' -subtract 0 -if - | mrthreshold - - -mask _crudecsfextra.mif | mrcalc _crudecsfextra.mif - 0 -if refined_csf.mif -datatype bit', show=False) + run.command(f'mrcalc _crudewmoutliers.mif safe_sdm.mif 0 -if {crudecsfmin} -gt 1 crude_csf.mif -if _crudecsfextra.mif -datatype bit', show=False) + run.command(f'mrcalc _crudecsfextra.mif safe_sdm.mif {crudecsfmin} -subtract 0 -if - | ' + 'mrthreshold - - -mask _crudecsfextra.mif | ' + 'mrcalc _crudecsfextra.mif - 0 -if refined_csf.mif -datatype bit', + show=False) statrefcsfcount = image.statistics('refined_csf.mif', mask='refined_csf.mif').count - app.console(' [ CSF: ' + str(statcrudecsfcount) + ' -> ' + str(statrefcsfcount) + ' ]') + app.console(f' [ CSF: {statcrudecsfcount} -> {statrefcsfcount} ]') # FINAL VOXEL SELECTION AND RESPONSE FUNCTION ESTIMATION @@ -207,42 +256,49 @@ def execute(): #pylint: disable=unused-variable # Get final voxels for CSF response function estimation from refined CSF. app.console('* CSF:') - app.console(' * Selecting final voxels (' + str(app.ARGS.csf) + '% of refined CSF)...') + app.console(f' * Selecting final voxels ({app.ARGS.csf}% of refined CSF)...') voxcsfcount = int(round(statrefcsfcount * app.ARGS.csf / 100.0)) - run.command('mrcalc refined_csf.mif safe_sdm.mif 0 -if - | mrthreshold - - -top ' + str(voxcsfcount) + ' -ignorezero | mrcalc refined_csf.mif - 0 -if - -datatype bit | mrconvert - voxels_csf.mif -axes 0,1,2', show=False) + run.command('mrcalc refined_csf.mif safe_sdm.mif 0 -if - | ' + f'mrthreshold - - -top {voxcsfcount} -ignorezero | ' + 'mrcalc refined_csf.mif - 0 -if - -datatype bit | ' + 'mrconvert - voxels_csf.mif -axes 0,1,2', + show=False) statvoxcsfcount = image.statistics('voxels_csf.mif', mask='voxels_csf.mif').count - app.console(' [ CSF: ' + str(statrefcsfcount) + ' -> ' + str(statvoxcsfcount) + ' ]') + app.console(f' [ CSF: {statrefcsfcount} -> {statvoxcsfcount} ]') # Estimate CSF response function app.console(' * Estimating response function...') - run.command('amp2response dwi.mif voxels_csf.mif safe_vecs.mif response_csf.txt' + bvalues_option + ' -isotropic', show=False) + run.command(f'amp2response dwi.mif voxels_csf.mif safe_vecs.mif response_csf.txt {bvalues_option} -isotropic', show=False) # Get final voxels for GM response function estimation from refined GM. app.console('* GM:') - app.console(' * Selecting final voxels (' + str(app.ARGS.gm) + '% of refined GM)...') + app.console(f' * Selecting final voxels ({app.ARGS.gm}% of refined GM)...') voxgmcount = int(round(statrefgmcount * app.ARGS.gm / 100.0)) refgmmedian = image.statistics('safe_sdm.mif', mask='refined_gm.mif').median - run.command('mrcalc refined_gm.mif safe_sdm.mif ' + str(refgmmedian) + ' -subtract -abs 1 -add 0 -if - | mrthreshold - - -bottom ' + str(voxgmcount) + ' -ignorezero | mrcalc refined_gm.mif - 0 -if - -datatype bit | mrconvert - voxels_gm.mif -axes 0,1,2', show=False) + run.command(f'mrcalc refined_gm.mif safe_sdm.mif {refgmmedian} -subtract -abs 1 -add 0 -if - | ' + f'mrthreshold - - -bottom {voxgmcount} -ignorezero | ' + 'mrcalc refined_gm.mif - 0 -if - -datatype bit | ' + 'mrconvert - voxels_gm.mif -axes 0,1,2', + show=False) statvoxgmcount = image.statistics('voxels_gm.mif', mask='voxels_gm.mif').count - app.console(' [ GM: ' + str(statrefgmcount) + ' -> ' + str(statvoxgmcount) + ' ]') + app.console(f' [ GM: {statrefgmcount} -> {statvoxgmcount} ]') # Estimate GM response function app.console(' * Estimating response function...') - run.command('amp2response dwi.mif voxels_gm.mif safe_vecs.mif response_gm.txt' + bvalues_option + ' -isotropic', show=False) + run.command(f'amp2response dwi.mif voxels_gm.mif safe_vecs.mif response_gm.txt {bvalues_option} -isotropic', show=False) # Get final voxels for single-fibre WM response function estimation from refined WM. app.console('* Single-fibre WM:') - app.console(' * Selecting final voxels' - + ('' if app.ARGS.wm_algo == 'tax' else (' ('+ str(app.ARGS.sfwm) + '% of refined WM)')) - + '...') + sfwm_message = '' if app.ARGS.wm_algo == "tax" else f' ({app.ARGS.sfwm}% of refined WM)' + app.console(f' * Selecting final voxels{sfwm_message}...') voxsfwmcount = int(round(statrefwmcount * app.ARGS.sfwm / 100.0)) if app.ARGS.wm_algo: recursive_cleanup_option='' if not app.DO_CLEANUP: recursive_cleanup_option = ' -nocleanup' - app.console(' Selecting WM single-fibre voxels using \'' + app.ARGS.wm_algo + '\' algorithm') + app.console(f' Selecting WM single-fibre voxels using "{app.ARGS.wm_algo}" algorithm') if app.ARGS.wm_algo == 'tax' and app.ARGS.sfwm != 0.5: app.warn('Single-fibre WM response function selection algorithm "tax" will not honour requested WM voxel percentage') - run.command('dwi2response ' + app.ARGS.wm_algo + ' dwi.mif _respsfwmss.txt -mask refined_wm.mif -voxels voxels_sfwm.mif' + run.command(f'dwi2response {app.ARGS.wm_algo} dwi.mif _respsfwmss.txt -mask refined_wm.mif -voxels voxels_sfwm.mif' + ('' if app.ARGS.wm_algo == 'tax' else (' -number ' + str(voxsfwmcount))) + ' -scratch ' + shlex.quote(app.SCRATCH_DIR) + recursive_cleanup_option, @@ -258,20 +314,20 @@ def execute(): #pylint: disable=unused-variable with open('ewmrf.txt', 'w', encoding='utf-8') as ewr: for iis in isiso: if iis: - ewr.write("%s 0 0 0\n" % refwmcoef) + ewr.write(f'{refwmcoef} 0 0 0\n') else: - ewr.write("%s -%s %s -%s\n" % (refwmcoef, refwmcoef, refwmcoef, refwmcoef)) + ewr.write(f'{refwmcoef} {-refwmcoef} {refwmcoef} {-refwmcoef}\n') run.command('dwi2fod msmt_csd dwi.mif ewmrf.txt abs_ewm2.mif response_csf.txt abs_csf2.mif -mask refined_wm.mif -lmax 2,0' + bvalues_option, show=False) run.command('mrconvert abs_ewm2.mif - -coord 3 0 | mrcalc - abs_csf2.mif -add abs_sum2.mif', show=False) run.command('sh2peaks abs_ewm2.mif - -num 1 -mask refined_wm.mif | peaks2amp - - | mrcalc - abs_sum2.mif -divide - | mrconvert - metric_sfwm2.mif -coord 3 0 -axes 0,1,2', show=False) - run.command('mrcalc refined_wm.mif metric_sfwm2.mif 0 -if - | mrthreshold - - -top ' + str(voxsfwmcount * 2) + ' -ignorezero | mrcalc refined_wm.mif - 0 -if - -datatype bit | mrconvert - refined_sfwm.mif -axes 0,1,2', show=False) + run.command(f'mrcalc refined_wm.mif metric_sfwm2.mif 0 -if - | mrthreshold - - -top {2*voxsfwmcount} -ignorezero | mrcalc refined_wm.mif - 0 -if - -datatype bit | mrconvert - refined_sfwm.mif -axes 0,1,2', show=False) run.command('dwi2fod msmt_csd dwi.mif ewmrf.txt abs_ewm6.mif response_csf.txt abs_csf6.mif -mask refined_sfwm.mif -lmax 6,0' + bvalues_option, show=False) run.command('mrconvert abs_ewm6.mif - -coord 3 0 | mrcalc - abs_csf6.mif -add abs_sum6.mif', show=False) run.command('sh2peaks abs_ewm6.mif - -num 1 -mask refined_sfwm.mif | peaks2amp - - | mrcalc - abs_sum6.mif -divide - | mrconvert - metric_sfwm6.mif -coord 3 0 -axes 0,1,2', show=False) - run.command('mrcalc refined_sfwm.mif metric_sfwm6.mif 0 -if - | mrthreshold - - -top ' + str(voxsfwmcount) + ' -ignorezero | mrcalc refined_sfwm.mif - 0 -if - -datatype bit | mrconvert - voxels_sfwm.mif -axes 0,1,2', show=False) + run.command(f'mrcalc refined_sfwm.mif metric_sfwm6.mif 0 -if - | mrthreshold - - -top {voxsfwmcount} -ignorezero | mrcalc refined_sfwm.mif - 0 -if - -datatype bit | mrconvert - voxels_sfwm.mif -axes 0,1,2', show=False) statvoxsfwmcount = image.statistics('voxels_sfwm.mif', mask='voxels_sfwm.mif').count - app.console(' [ WM: ' + str(statrefwmcount) + ' -> ' + str(statvoxsfwmcount) + ' (single-fibre) ]') + app.console(f' [ WM: {statrefwmcount} -> {statvoxsfwmcount} (single-fibre) ]') # Estimate SF WM response function app.console(' * Estimating response function...') run.command('amp2response dwi.mif voxels_sfwm.mif safe_vecs.mif response_sfwm.txt' + bvalues_option + sfwm_lmax_option, show=False) @@ -287,12 +343,12 @@ def execute(): #pylint: disable=unused-variable run.command('mrcat voxels_csf.mif voxels_gm.mif voxels_sfwm.mif check_voxels.mif -axis 3', show=False) # Copy results to output files - run.function(shutil.copyfile, 'response_sfwm.txt', path.from_user(app.ARGS.out_sfwm, False), show=False) - run.function(shutil.copyfile, 'response_gm.txt', path.from_user(app.ARGS.out_gm, False), show=False) - run.function(shutil.copyfile, 'response_csf.txt', path.from_user(app.ARGS.out_csf, False), show=False) + run.function(shutil.copyfile, 'response_sfwm.txt', app.ARGS.out_sfwm, show=False) + run.function(shutil.copyfile, 'response_gm.txt', app.ARGS.out_gm, show=False) + run.function(shutil.copyfile, 'response_csf.txt', app.ARGS.out_csf, show=False) if app.ARGS.voxels: - run.command('mrconvert check_voxels.mif ' + path.from_user(app.ARGS.voxels), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'check_voxels.mif', app.ARGS.voxels], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True, show=False) diff --git a/lib/mrtrix3/dwi2response/fa.py b/lib/mrtrix3/dwi2response/fa.py index b0710bc0fd..e08767b492 100644 --- a/lib/mrtrix3/dwi2response/fa.py +++ b/lib/mrtrix3/dwi2response/fa.py @@ -15,68 +15,71 @@ import shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run +NEEDS_SINGLE_SHELL = False # pylint: disable=unused-variable +SUPPORTS_MASK = True # pylint: disable=unused-variable + def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('fa', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the old FA-threshold heuristic for single-fibre voxel selection and response function estimation') - parser.add_citation('Tournier, J.-D.; Calamante, F.; Gadian, D. G. & Connelly, A. Direct estimation of the fiber orientation density function from diffusion-weighted MRI data using spherical deconvolution. NeuroImage, 2004, 23, 1176-1185') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') - options = parser.add_argument_group('Options specific to the \'fa\' algorithm') - options.add_argument('-erode', type=app.Parser.Int(0), metavar='passes', default=3, help='Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually)') - options.add_argument('-number', type=app.Parser.Int(1), metavar='voxels', default=300, help='The number of highest-FA voxels to use') - options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), metavar='value', help='Apply a hard FA threshold, rather than selecting the top voxels') + parser.add_citation('Tournier, J.-D.; Calamante, F.; Gadian, D. G. & Connelly, A. ' + 'Direct estimation of the fiber orientation density function from diffusion-weighted MRI data using spherical deconvolution. ' + 'NeuroImage, 2004, 23, 1176-1185') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI') + parser.add_argument('output', + type=app.Parser.FileOut(), + help='The output response function text file') + options = parser.add_argument_group('Options specific to the "fa" algorithm') + options.add_argument('-erode', + type=app.Parser.Int(0), + metavar='passes', + default=3, + help='Number of brain mask erosion steps to apply prior to threshold ' + '(not used if mask is provided manually)') + options.add_argument('-number', + type=app.Parser.Int(1), + metavar='voxels', + default=300, + help='The number of highest-FA voxels to use') + options.add_argument('-threshold', + type=app.Parser.Float(0.0, 1.0), + metavar='value', + help='Apply a hard FA threshold, ' + 'rather than selecting the top voxels') parser.flag_mutually_exclusive_options( [ 'number', 'threshold' ] ) -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_single_shell(): #pylint: disable=unused-variable - return False - - - -def supports_mask(): #pylint: disable=unused-variable - return True - - - def execute(): #pylint: disable=unused-variable bvalues = [ int(round(float(x))) for x in image.mrinfo('dwi.mif', 'shell_bvalues').split() ] if len(bvalues) < 2: raise MRtrixError('Need at least 2 unique b-values (including b=0).') lmax_option = '' if app.ARGS.lmax: - lmax_option = ' -lmax ' + ','.join(str(item) for item in app.ARGS.lmax) + lmax_option = f' -lmax {",".join(map(str, app.ARGS.lmax))}' if not app.ARGS.mask: - run.command('maskfilter mask.mif erode mask_eroded.mif -npass ' + str(app.ARGS.erode)) + run.command(f'maskfilter mask.mif erode mask_eroded.mif -npass {app.ARGS.erode}') mask_path = 'mask_eroded.mif' else: mask_path = 'mask.mif' - run.command('dwi2tensor dwi.mif -mask ' + mask_path + ' tensor.mif') - run.command('tensor2metric tensor.mif -fa fa.mif -vector vector.mif -mask ' + mask_path) + run.command(f'dwi2tensor dwi.mif -mask {mask_path} tensor.mif') + run.command(f'tensor2metric tensor.mif -fa fa.mif -vector vector.mif -mask {mask_path}') if app.ARGS.threshold: - run.command('mrthreshold fa.mif voxels.mif -abs ' + str(app.ARGS.threshold)) + run.command(f'mrthreshold fa.mif voxels.mif -abs {app.ARGS.threshold}') else: - run.command('mrthreshold fa.mif voxels.mif -top ' + str(app.ARGS.number)) - run.command('dwiextract dwi.mif - -singleshell -no_bzero | amp2response - voxels.mif vector.mif response.txt' + lmax_option) + run.command(f'mrthreshold fa.mif voxels.mif -top {app.ARGS.number}') + run.command('dwiextract dwi.mif - -singleshell -no_bzero | ' + f'amp2response - voxels.mif vector.mif response.txt {lmax_option}') - run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) + run.function(shutil.copyfile, 'response.txt', app.ARGS.output) if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'voxels.mif', app.ARGS.voxels], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/manual.py b/lib/mrtrix3/dwi2response/manual.py index aa6cdf50dd..0f36981cfb 100644 --- a/lib/mrtrix3/dwi2response/manual.py +++ b/lib/mrtrix3/dwi2response/manual.py @@ -15,75 +15,73 @@ import os, shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run +NEEDS_SINGLE_SHELL = False # pylint: disable=unused-variable +SUPPORTS_MASK = False # pylint: disable=unused-variable def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('manual', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - parser.set_synopsis('Derive a response function using an input mask image alone (i.e. pre-selected voxels)') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('in_voxels', type=app.Parser.ImageIn(), help='Input voxel selection mask') - parser.add_argument('output', type=app.Parser.FileOut(), help='Output response function text file') - options = parser.add_argument_group('Options specific to the \'manual\' algorithm') - options.add_argument('-dirs', type=app.Parser.ImageIn(), metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') + parser.set_synopsis('Derive a response function using an input mask image alone ' + '(i.e. pre-selected voxels)') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI') + parser.add_argument('in_voxels', + type=app.Parser.ImageIn(), + help='Input voxel selection mask') + parser.add_argument('output', + type=app.Parser.FileOut(), + help='Output response function text file') + options = parser.add_argument_group('Options specific to the "manual" algorithm') + options.add_argument('-dirs', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide an input image that contains a pre-estimated fibre direction in each voxel ' + '(a tensor fit will be used otherwise)') -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - +def execute(): #pylint: disable=unused-variable + # TODO Can usage() wipe this from the CLI? + if os.path.exists('mask.mif'): + app.warn('-mask option is ignored by algorithm "manual"') + os.remove('mask.mif') -def get_inputs(): #pylint: disable=unused-variable - mask_path = path.to_scratch('mask.mif', False) - if os.path.exists(mask_path): - app.warn('-mask option is ignored by algorithm \'manual\'') - os.remove(mask_path) - run.command('mrconvert ' + path.from_user(app.ARGS.in_voxels) + ' ' + path.to_scratch('in_voxels.mif'), + run.command(['mrconvert', app.ARGS.in_voxels, 'in_voxels.mif'], preserve_pipes=True) if app.ARGS.dirs: - run.command('mrconvert ' + path.from_user(app.ARGS.dirs) + ' ' + path.to_scratch('dirs.mif') + ' -strides 0,0,0,1', + run.command(['mrconvert', app.ARGS.dirs, 'dirs.mif', '-strides', '0,0,0,1'], preserve_pipes=True) - - -def needs_single_shell(): #pylint: disable=unused-variable - return False - - - -def supports_mask(): #pylint: disable=unused-variable - return False - - - -def execute(): #pylint: disable=unused-variable shells = [ int(round(float(x))) for x in image.mrinfo('dwi.mif', 'shell_bvalues').split() ] - bvalues_option = ' -shells ' + ','.join(map(str,shells)) + bvalues_option = f' -shells {",".join(map(str,shells))}' # Get lmax information (if provided) lmax_option = '' if app.ARGS.lmax: if len(app.ARGS.lmax) != len(shells): - raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(app.ARGS.lmax)) + ') does not match number of b-value shells (' + str(len(shells)) + ')') + raise MRtrixError(f'Number of manually-defined lmax\'s ({len(app.ARGS.lmax)}) ' + f'does not match number of b-value shells ({len(shells)})') if any(l % 2 for l in app.ARGS.lmax): raise MRtrixError('Values for lmax must be even') if any(l < 0 for l in app.ARGS.lmax): raise MRtrixError('Values for lmax must be non-negative') - lmax_option = ' -lmax ' + ','.join(map(str,app.ARGS.lmax)) + lmax_option = f' -lmax {",".join(map(str,app.ARGS.lmax))}' # Do we have directions, or do we need to calculate them? if not os.path.exists('dirs.mif'): - run.command('dwi2tensor dwi.mif - -mask in_voxels.mif | tensor2metric - -vector dirs.mif') + run.command('dwi2tensor dwi.mif - -mask in_voxels.mif | ' + 'tensor2metric - -vector dirs.mif') # Get response function run.command('amp2response dwi.mif in_voxels.mif dirs.mif response.txt' + bvalues_option + lmax_option) - run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) + run.function(shutil.copyfile, 'response.txt', app.ARGS.output) if app.ARGS.voxels: - run.command('mrconvert in_voxels.mif ' + path.from_user(app.ARGS.voxels), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'in_voxels.mif', app.ARGS.voxels], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/msmt_5tt.py b/lib/mrtrix3/dwi2response/msmt_5tt.py index 50037f0622..0b01e2ca13 100644 --- a/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -15,9 +15,11 @@ import os, shlex, shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run +NEEDS_SINGLE_SHELL = False # pylint: disable=unused-variable +SUPPORTS_MASK = True # pylint: disable=unused-variable WM_ALGOS = [ 'fa', 'tax', 'tournier' ] @@ -27,51 +29,67 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('msmt_5tt', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Derive MSMT-CSD tissue response functions based on a co-registered five-tissue-type (5TT) image') - parser.add_citation('Jeurissen, B.; Tournier, J.-D.; Dhollander, T.; Connelly, A. & Sijbers, J. Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. NeuroImage, 2014, 103, 411-426') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('in_5tt', type=app.Parser.ImageIn(), help='Input co-registered 5TT image') - parser.add_argument('out_wm', type=app.Parser.FileOut(), help='Output WM response text file') - parser.add_argument('out_gm', type=app.Parser.FileOut(), help='Output GM response text file') - parser.add_argument('out_csf', type=app.Parser.FileOut(), help='Output CSF response text file') - options = parser.add_argument_group('Options specific to the \'msmt_5tt\' algorithm') - options.add_argument('-dirs', type=app.Parser.ImageIn(), metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise)') - options.add_argument('-fa', type=app.Parser.Float(0.0, 1.0), metavar='value', default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection (default: 0.2)') - options.add_argument('-pvf', type=app.Parser.Float(0.0, 1.0), metavar='fraction', default=0.95, help='Partial volume fraction threshold for tissue voxel selection (default: 0.95)') - options.add_argument('-wm_algo', metavar='algorithm', choices=WM_ALGOS, default='tournier', help='dwi2response algorithm to use for WM single-fibre voxel selection (options: ' + ', '.join(WM_ALGOS) + '; default: tournier)') - options.add_argument('-sfwm_fa_threshold', type=app.Parser.Float(0.0, 1.0), metavar='value', help='Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, which is passed to the -threshold option of the fa algorithm (warning: overrides -wm_algo option)') + parser.add_citation('Jeurissen, B.; Tournier, J.-D.; Dhollander, T.; Connelly, A. & Sijbers, J. ' + 'Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. ' + 'NeuroImage, 2014, 103, 411-426') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI') + parser.add_argument('in_5tt', + type=app.Parser.ImageIn(), + help='Input co-registered 5TT image') + parser.add_argument('out_wm', + type=app.Parser.FileOut(), + help='Output WM response text file') + parser.add_argument('out_gm', + type=app.Parser.FileOut(), + help='Output GM response text file') + parser.add_argument('out_csf', + type=app.Parser.FileOut(), + help='Output CSF response text file') + options = parser.add_argument_group('Options specific to the "msmt_5tt" algorithm') + options.add_argument('-dirs', + type=app.Parser.ImageIn(), + metavar='image', + help='Provide an input image that contains a pre-estimated fibre direction in each voxel ' + '(a tensor fit will be used otherwise)') + options.add_argument('-fa', + type=app.Parser.Float(0.0, 1.0), + metavar='value', + default=0.2, + help='Upper fractional anisotropy threshold for GM and CSF voxel selection ' + '(default: 0.2)') + options.add_argument('-pvf', + type=app.Parser.Float(0.0, 1.0), + metavar='fraction', + default=0.95, + help='Partial volume fraction threshold for tissue voxel selection ' + '(default: 0.95)') + options.add_argument('-wm_algo', + metavar='algorithm', + choices=WM_ALGOS, + default='tournier', + help='dwi2response algorithm to use for WM single-fibre voxel selection ' + f'(options: {", ".join(WM_ALGOS)}; default: tournier)') + options.add_argument('-sfwm_fa_threshold', + type=app.Parser.Float(0.0, 1.0), + metavar='value', + help='Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, ' + 'which is passed to the -threshold option of the fa algorithm ' + '(warning: overrides -wm_algo option)') -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.out_wm) - app.check_output_path(app.ARGS.out_gm) - app.check_output_path(app.ARGS.out_csf) - - +def execute(): #pylint: disable=unused-variable + # Ideally want to use the oversampling-based regridding of the 5TT image from the SIFT model, not mrtransform + # May need to commit 5ttregrid... -def get_inputs(): #pylint: disable=unused-variable - run.command('mrconvert ' + path.from_user(app.ARGS.in_5tt) + ' ' + path.to_scratch('5tt.mif'), + run.command(['mrconvert', app.ARGS.in_5tt, '5tt.mif'], preserve_pipes=True) if app.ARGS.dirs: - run.command('mrconvert ' + path.from_user(app.ARGS.dirs) + ' ' + path.to_scratch('dirs.mif') + ' -strides 0,0,0,1', + run.command(['mrconvert', app.ARGS.dirs, 'dirs.mif', '-strides', '0,0,0,1'], preserve_pipes=True) - - -def needs_single_shell(): #pylint: disable=unused-variable - return False - - - -def supports_mask(): #pylint: disable=unused-variable - return True - - - -def execute(): #pylint: disable=unused-variable - # Ideally want to use the oversampling-based regridding of the 5TT image from the SIFT model, not mrtransform - # May need to commit 5ttregrid... - # Verify input 5tt image verification_text = '' try: @@ -79,7 +97,7 @@ def execute(): #pylint: disable=unused-variable except run.MRtrixCmdError as except_5ttcheck: verification_text = except_5ttcheck.stderr if 'WARNING' in verification_text or 'ERROR' in verification_text: - app.warn('Command 5ttcheck indicates problems with provided input 5TT image \'' + app.ARGS.in_5tt + '\':') + app.warn(f'Command 5ttcheck indicates problems with provided input 5TT image "{app.ARGS.in_5tt}":') for line in verification_text.splitlines(): app.warn(line) app.warn('These may or may not interfere with the dwi2response msmt_5tt script') @@ -87,39 +105,49 @@ def execute(): #pylint: disable=unused-variable # Get shell information shells = [ int(round(float(x))) for x in image.mrinfo('dwi.mif', 'shell_bvalues').split() ] if len(shells) < 3: - app.warn('Less than three b-values; response functions will not be applicable in resolving three tissues using MSMT-CSD algorithm') + app.warn('Less than three b-values; ' + 'response functions will not be applicable in resolving three tissues using MSMT-CSD algorithm') # Get lmax information (if provided) sfwm_lmax_option = '' if app.ARGS.lmax: if len(app.ARGS.lmax) != len(shells): - raise MRtrixError('Number of manually-defined lmax\'s (' + str(len(app.ARGS.lmax)) + ') does not match number of b-values (' + str(len(shells)) + ')') + raise MRtrixError(f'Number of manually-defined lmax\'s ({len(app.ARGS.lmax)}) ' + f'does not match number of b-values ({len(shells)})') if any(l % 2 for l in app.ARGS.lmax): raise MRtrixError('Values for lmax must be even') if any(l < 0 for l in app.ARGS.lmax): raise MRtrixError('Values for lmax must be non-negative') sfwm_lmax_option = ' -lmax ' + ','.join(map(str,app.ARGS.lmax)) - run.command('dwi2tensor dwi.mif - -mask mask.mif | tensor2metric - -fa fa.mif -vector vector.mif') + run.command('dwi2tensor dwi.mif - -mask mask.mif | ' + 'tensor2metric - -fa fa.mif -vector vector.mif') if not os.path.exists('dirs.mif'): run.function(shutil.copy, 'vector.mif', 'dirs.mif') run.command('mrtransform 5tt.mif 5tt_regrid.mif -template fa.mif -interp linear') # Basic tissue masks - run.command('mrconvert 5tt_regrid.mif - -coord 3 2 -axes 0,1,2 | mrcalc - ' + str(app.ARGS.pvf) + ' -gt mask.mif -mult wm_mask.mif') - run.command('mrconvert 5tt_regrid.mif - -coord 3 0 -axes 0,1,2 | mrcalc - ' + str(app.ARGS.pvf) + ' -gt fa.mif ' + str(app.ARGS.fa) + ' -lt -mult mask.mif -mult gm_mask.mif') - run.command('mrconvert 5tt_regrid.mif - -coord 3 3 -axes 0,1,2 | mrcalc - ' + str(app.ARGS.pvf) + ' -gt fa.mif ' + str(app.ARGS.fa) + ' -lt -mult mask.mif -mult csf_mask.mif') + run.command('mrconvert 5tt_regrid.mif - -coord 3 2 -axes 0,1,2 | ' + f'mrcalc - {app.ARGS.pvf} -gt mask.mif -mult wm_mask.mif') + run.command('mrconvert 5tt_regrid.mif - -coord 3 0 -axes 0,1,2 | ' + f'mrcalc - {app.ARGS.pvf} -gt fa.mif {app.ARGS.fa} -lt -mult mask.mif -mult gm_mask.mif') + run.command('mrconvert 5tt_regrid.mif - -coord 3 3 -axes 0,1,2 | ' + f'mrcalc - {app.ARGS.pvf} -gt fa.mif {app.ARGS.fa} -lt -mult mask.mif -mult csf_mask.mif') # Revise WM mask to only include single-fibre voxels recursive_cleanup_option='' if not app.DO_CLEANUP: recursive_cleanup_option = ' -nocleanup' if not app.ARGS.sfwm_fa_threshold: - app.console('Selecting WM single-fibre voxels using \'' + app.ARGS.wm_algo + '\' algorithm') - run.command('dwi2response ' + app.ARGS.wm_algo + ' dwi.mif wm_ss_response.txt -mask wm_mask.mif -voxels wm_sf_mask.mif -scratch ' + shlex.quote(app.SCRATCH_DIR) + recursive_cleanup_option) + app.console(f'Selecting WM single-fibre voxels using "{app.ARGS.wm_algo}" algorithm') + run.command(f'dwi2response {app.ARGS.wm_algo} dwi.mif wm_ss_response.txt -mask wm_mask.mif -voxels wm_sf_mask.mif' + ' -scratch %s' % shlex.quote(app.SCRATCH_DIR) + + recursive_cleanup_option) else: - app.console('Selecting WM single-fibre voxels using \'fa\' algorithm with a hard FA threshold of ' + str(app.ARGS.sfwm_fa_threshold)) - run.command('dwi2response fa dwi.mif wm_ss_response.txt -mask wm_mask.mif -threshold ' + str(app.ARGS.sfwm_fa_threshold) + ' -voxels wm_sf_mask.mif -scratch ' + shlex.quote(app.SCRATCH_DIR) + recursive_cleanup_option) + app.console(f'Selecting WM single-fibre voxels using "fa" algorithm with a hard FA threshold of {app.ARGS.sfwm_fa_threshold}') + run.command(f'dwi2response fa dwi.mif wm_ss_response.txt -mask wm_mask.mif -threshold {app.ARGS.sfwm_fa_threshold} -voxels wm_sf_mask.mif' + ' -scratch %s' % shlex.quote(app.SCRATCH_DIR) + + recursive_cleanup_option) # Check for empty masks wm_voxels = image.statistics('wm_sf_mask.mif', mask='wm_sf_mask.mif').count @@ -133,28 +161,22 @@ def execute(): #pylint: disable=unused-variable if not csf_voxels: empty_masks.append('CSF') if empty_masks: - message = ','.join(empty_masks) - message += ' tissue mask' - if len(empty_masks) > 1: - message += 's' - message += ' empty; cannot estimate response function' - if len(empty_masks) > 1: - message += 's' - raise MRtrixError(message) + raise MRtrixError(f'{",".join(empty_masks)} tissue {("masks" if len(empty_masks) > 1 else "mask")} empty; ' + f'cannot estimate response {"functions" if len(empty_masks) > 1 else "function"}') # For each of the three tissues, generate a multi-shell response - bvalues_option = ' -shells ' + ','.join(map(str,shells)) - run.command('amp2response dwi.mif wm_sf_mask.mif dirs.mif wm.txt' + bvalues_option + sfwm_lmax_option) - run.command('amp2response dwi.mif gm_mask.mif dirs.mif gm.txt' + bvalues_option + ' -isotropic') - run.command('amp2response dwi.mif csf_mask.mif dirs.mif csf.txt' + bvalues_option + ' -isotropic') - run.function(shutil.copyfile, 'wm.txt', path.from_user(app.ARGS.out_wm, False)) - run.function(shutil.copyfile, 'gm.txt', path.from_user(app.ARGS.out_gm, False)) - run.function(shutil.copyfile, 'csf.txt', path.from_user(app.ARGS.out_csf, False)) + bvalues_option = f' -shells {",".join(map(str,shells))}' + run.command(f'amp2response dwi.mif wm_sf_mask.mif dirs.mif wm.txt {bvalues_option} {sfwm_lmax_option}') + run.command(f'amp2response dwi.mif gm_mask.mif dirs.mif gm.txt {bvalues_option} -isotropic') + run.command(f'amp2response dwi.mif csf_mask.mif dirs.mif csf.txt {bvalues_option} -isotropic') + run.function(shutil.copyfile, 'wm.txt', app.ARGS.out_wm) + run.function(shutil.copyfile, 'gm.txt', app.ARGS.out_gm) + run.function(shutil.copyfile, 'csf.txt', app.ARGS.out_csf) # Generate output 4D binary image with voxel selections; RGB as in MSMT-CSD paper run.command('mrcat csf_mask.mif gm_mask.mif wm_sf_mask.mif voxels.mif -axis 3') if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'voxels.mif', app.ARGS.voxels], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/tax.py b/lib/mrtrix3/dwi2response/tax.py index dbab94380d..18400c335a 100644 --- a/lib/mrtrix3/dwi2response/tax.py +++ b/lib/mrtrix3/dwi2response/tax.py @@ -15,48 +15,49 @@ import math, os, shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, image, matrix, path, run +from mrtrix3 import app, image, matrix, run +NEEDS_SINGLE_SHELL = True # pylint: disable=unused-variable +SUPPORTS_MASK = True # pylint: disable=unused-variable def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('tax', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the Tax et al. (2014) recursive calibration algorithm for single-fibre voxel selection and response function estimation') - parser.add_citation('Tax, C. M.; Jeurissen, B.; Vos, S. B.; Viergever, M. A. & Leemans, A. Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. NeuroImage, 2014, 86, 67-80') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') - options = parser.add_argument_group('Options specific to the \'tax\' algorithm') - options.add_argument('-peak_ratio', type=app.Parser.Float(0.0, 1.0), metavar='value', default=0.1, help='Second-to-first-peak amplitude ratio threshold') - options.add_argument('-max_iters', type=app.Parser.Int(0), metavar='iterations', default=20, help='Maximum number of iterations (set to 0 to force convergence)') - options.add_argument('-convergence', type=app.Parser.Float(0.0), metavar='percentage', default=0.5, help='Percentile change in any RF coefficient required to continue iterating') - - - -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_single_shell(): #pylint: disable=unused-variable - return True - - - -def supports_mask(): #pylint: disable=unused-variable - return True + parser.add_citation('Tax, C. M.; Jeurissen, B.; Vos, S. B.; Viergever, M. A. & Leemans, A. ' + 'Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. ' + 'NeuroImage, 2014, 86, 67-80') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI') + parser.add_argument('output', + type=app.Parser.FileOut(), + help='The output response function text file') + options = parser.add_argument_group('Options specific to the "tax" algorithm') + options.add_argument('-peak_ratio', + type=app.Parser.Float(0.0, 1.0), + metavar='value', + default=0.1, + help='Second-to-first-peak amplitude ratio threshold') + options.add_argument('-max_iters', + type=app.Parser.Int(0), + metavar='iterations', + default=20, + help='Maximum number of iterations ' + '(set to 0 to force convergence)') + options.add_argument('-convergence', + type=app.Parser.Float(0.0), + metavar='percentage', + default=0.5, + help='Percentile change in any RF coefficient required to continue iterating') def execute(): #pylint: disable=unused-variable lmax_option = '' if app.ARGS.lmax: - lmax_option = ' -lmax ' + ','.join(str(item) for item in app.ARGS.lmax) + lmax_option = ' -lmax ' + ','.join(map(str, app.ARGS.lmax)) convergence_change = 0.01 * app.ARGS.convergence @@ -64,7 +65,7 @@ def execute(): #pylint: disable=unused-variable iteration = 0 while iteration < app.ARGS.max_iters or not app.ARGS.max_iters: - prefix = 'iter' + str(iteration) + '_' + prefix = f'iter{iteration}_' # How to initialise response function? # old dwi2response command used mean & standard deviation of DWI data; however @@ -88,36 +89,39 @@ def execute(): #pylint: disable=unused-variable with open('init_RF.txt', 'w', encoding='utf-8') as init_rf_file: init_rf_file.write(' '.join(init_rf)) else: - rf_in_path = 'iter' + str(iteration-1) + '_RF.txt' - mask_in_path = 'iter' + str(iteration-1) + '_SF.mif' + rf_in_path = f'iter{iteration-1}_RF.txt' + mask_in_path = f'iter{iteration-1}_SF.mif' # Run CSD - run.command('dwi2fod csd dwi.mif ' + rf_in_path + ' ' + prefix + 'FOD.mif -mask ' + mask_in_path) + run.command(f'dwi2fod csd dwi.mif {rf_in_path} {prefix}FOD.mif -mask {mask_in_path}') # Get amplitudes of two largest peaks, and directions of largest - run.command('fod2fixel ' + prefix + 'FOD.mif ' + prefix + 'fixel -peak peaks.mif -mask ' + mask_in_path + ' -fmls_no_thresholds') - app.cleanup(prefix + 'FOD.mif') - run.command('fixel2voxel ' + prefix + 'fixel/peaks.mif none ' + prefix + 'amps.mif') - run.command('mrconvert ' + prefix + 'amps.mif ' + prefix + 'first_peaks.mif -coord 3 0 -axes 0,1,2') - run.command('mrconvert ' + prefix + 'amps.mif ' + prefix + 'second_peaks.mif -coord 3 1 -axes 0,1,2') - app.cleanup(prefix + 'amps.mif') - run.command('fixel2peaks ' + prefix + 'fixel/directions.mif ' + prefix + 'first_dir.mif -number 1') - app.cleanup(prefix + 'fixel') + run.command(f'fod2fixel {prefix}FOD.mif {prefix}fixel -peak peaks.mif -mask {mask_in_path} -fmls_no_thresholds') + app.cleanup(f'{prefix}FOD.mif') + run.command(f'fixel2voxel {prefix}fixel/peaks.mif none {prefix}amps.mif') + run.command(f'mrconvert {prefix}amps.mif {prefix}first_peaks.mif -coord 3 0 -axes 0,1,2') + run.command(f'mrconvert {prefix}amps.mif {prefix}second_peaks.mif -coord 3 1 -axes 0,1,2') + app.cleanup(f'{prefix}amps.mif') + run.command(f'fixel2peaks {prefix}fixel/directions.mif {prefix}first_dir.mif -number 1') + app.cleanup(f'{prefix}fixel') # Revise single-fibre voxel selection based on ratio of tallest to second-tallest peak - run.command('mrcalc ' + prefix + 'second_peaks.mif ' + prefix + 'first_peaks.mif -div ' + prefix + 'peak_ratio.mif') - app.cleanup(prefix + 'first_peaks.mif') - app.cleanup(prefix + 'second_peaks.mif') - run.command('mrcalc ' + prefix + 'peak_ratio.mif ' + str(app.ARGS.peak_ratio) + ' -lt ' + mask_in_path + ' -mult ' + prefix + 'SF.mif -datatype bit') - app.cleanup(prefix + 'peak_ratio.mif') + run.command(f'mrcalc {prefix}second_peaks.mif {prefix}first_peaks.mif -div {prefix}peak_ratio.mif') + app.cleanup(f'{prefix}first_peaks.mif') + app.cleanup(f'{prefix}second_peaks.mif') + run.command(f'mrcalc {prefix}peak_ratio.mif {app.ARGS.peak_ratio} -lt {mask_in_path} -mult {prefix}SF.mif -datatype bit') + app.cleanup(f'{prefix}peak_ratio.mif') # Make sure image isn't empty - sf_voxel_count = image.statistics(prefix + 'SF.mif', mask=prefix+'SF.mif').count + sf_voxel_count = image.statistics(prefix + 'SF.mif', mask=f'{prefix}SF.mif').count if not sf_voxel_count: raise MRtrixError('Aborting: All voxels have been excluded from single-fibre selection') # Generate a new response function - run.command('amp2response dwi.mif ' + prefix + 'SF.mif ' + prefix + 'first_dir.mif ' + prefix + 'RF.txt' + lmax_option) - app.cleanup(prefix + 'first_dir.mif') + run.command(f'amp2response dwi.mif {prefix}SF.mif {prefix}first_dir.mif {prefix}RF.txt {lmax_option}') + app.cleanup(f'{prefix}first_dir.mif') - new_rf = matrix.load_vector(prefix + 'RF.txt') - progress.increment('Optimising (' + str(iteration+1) + ' iterations, ' + str(sf_voxel_count) + ' voxels, RF: [ ' + ', '.join('{:.3f}'.format(n) for n in new_rf) + '] )') + new_rf = matrix.load_vector(f'{prefix}RF.txt') + rf_string = ', '.join(f'{n:.3f}' for n in new_rf) + progress.increment(f'Optimising ({iteration+1} iterations, ' + f'{sf_voxel_count} voxels, ' + f'RF: [ {rf_string} ]' ) # Detect convergence # Look for a change > some percentage - don't bother looking at the masks @@ -131,8 +135,8 @@ def execute(): #pylint: disable=unused-variable if ratio > convergence_change: reiterate = True if not reiterate: - run.function(shutil.copyfile, prefix + 'RF.txt', 'response.txt') - run.function(shutil.copyfile, prefix + 'SF.mif', 'voxels.mif') + run.function(shutil.copyfile, f'{prefix}RF.txt', 'response.txt') + run.function(shutil.copyfile, f'{prefix}SF.mif', 'voxels.mif') break app.cleanup(rf_in_path) @@ -144,15 +148,15 @@ def execute(): #pylint: disable=unused-variable # If we've terminated due to hitting the iteration limiter, we still need to copy the output file(s) to the correct location if os.path.exists('response.txt'): - app.console('Exited at iteration ' + str(iteration+1) + ' with ' + str(sf_voxel_count) + ' SF voxels due to unchanged RF coefficients') + app.console(f'Exited at iteration {iteration+1} with {sf_voxel_count} SF voxels due to unchanged RF coefficients') else: - app.console('Exited after maximum ' + str(app.ARGS.max_iters) + ' iterations with ' + str(sf_voxel_count) + ' SF voxels') - run.function(shutil.copyfile, 'iter' + str(app.ARGS.max_iters-1) + '_RF.txt', 'response.txt') - run.function(shutil.copyfile, 'iter' + str(app.ARGS.max_iters-1) + '_SF.mif', 'voxels.mif') + app.console(f'Exited after maximum {app.ARGS.max_iters} iterations with {sf_voxel_count} SF voxels') + run.function(shutil.copyfile, f'iter{app.ARGS.max_iters-1}_RF.txt', 'response.txt') + run.function(shutil.copyfile, f'iter{app.ARGS.max_iters-1}_SF.mif', 'voxels.mif') - run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) + run.function(shutil.copyfile, 'response.txt', app.ARGS.output) if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'voxels.mif', app.ARGS.voxels], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwi2response/tournier.py b/lib/mrtrix3/dwi2response/tournier.py index e6fb866c16..5659c24608 100644 --- a/lib/mrtrix3/dwi2response/tournier.py +++ b/lib/mrtrix3/dwi2response/tournier.py @@ -15,49 +15,55 @@ import os, shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, image, matrix, path, run +from mrtrix3 import app, image, matrix, run +NEEDS_SINGLE_SHELL = True # pylint: disable=unused-variable +SUPPORTS_MASK = True # pylint: disable=unused-variable def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('tournier', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Use the Tournier et al. (2013) iterative algorithm for single-fibre voxel selection and response function estimation') - parser.add_citation('Tournier, J.-D.; Calamante, F. & Connelly, A. Determination of the appropriate b-value and number of gradient directions for high-angular-resolution diffusion-weighted imaging. NMR Biomedicine, 2013, 26, 1775-1786') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI') - parser.add_argument('output', type=app.Parser.FileOut(), help='The output response function text file') - options = parser.add_argument_group('Options specific to the \'tournier\' algorithm') - options.add_argument('-number', type=app.Parser.Int(1), metavar='voxels', default=300, help='Number of single-fibre voxels to use when calculating response function') - options.add_argument('-iter_voxels', type=app.Parser.Int(0), metavar='voxels', default=0, help='Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number)') - options.add_argument('-dilate', type=app.Parser.Int(1), metavar='passes', default=1, help='Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration') - options.add_argument('-max_iters', type=app.Parser.Int(0), metavar='iterations', default=10, help='Maximum number of iterations (set to 0 to force convergence)') - - - -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - - -def get_inputs(): #pylint: disable=unused-variable - pass - - - -def needs_single_shell(): #pylint: disable=unused-variable - return True - - - -def supports_mask(): #pylint: disable=unused-variable - return True + parser.add_citation('Tournier, J.-D.; Calamante, F. & Connelly, A. ' + 'Determination of the appropriate b-value and number of gradient directions for high-angular-resolution diffusion-weighted imaging. ' + 'NMR Biomedicine, 2013, 26, 1775-1786') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI') + parser.add_argument('output', + type=app.Parser.FileOut(), + help='The output response function text file') + options = parser.add_argument_group('Options specific to the "tournier" algorithm') + options.add_argument('-number', + type=app.Parser.Int(1), + metavar='voxels', + default=300, + help='Number of single-fibre voxels to use when calculating response function') + options.add_argument('-iter_voxels', + type=app.Parser.Int(0), + metavar='voxels', + default=0, + help='Number of single-fibre voxels to select when preparing for the next iteration ' + '(default = 10 x value given in -number)') + options.add_argument('-dilate', + type=app.Parser.Int(1), + metavar='passes', + default=1, + help='Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration') + options.add_argument('-max_iters', + type=app.Parser.Int(0), + metavar='iterations', + default=10, + help='Maximum number of iterations ' + '(set to 0 to force convergence)') def execute(): #pylint: disable=unused-variable lmax_option = '' if app.ARGS.lmax: - lmax_option = ' -lmax ' + ','.join(str(item) for item in app.ARGS.lmax) + lmax_option = f' -lmax {",".join(map(str, app.ARGS.lmax))}' progress = app.ProgressBar('Optimising') @@ -65,11 +71,12 @@ def execute(): #pylint: disable=unused-variable if iter_voxels == 0: iter_voxels = 10*app.ARGS.number elif iter_voxels < app.ARGS.number: - raise MRtrixError ('Number of selected voxels (-iter_voxels) must be greater than number of voxels desired (-number)') + raise MRtrixError ('Number of selected voxels (-iter_voxels) ' + 'must be greater than number of voxels desired (-number)') iteration = 0 while iteration < app.ARGS.max_iters or not app.ARGS.max_iters: - prefix = 'iter' + str(iteration) + '_' + prefix = f'iter{iteration}_' if iteration == 0: rf_in_path = 'init_RF.txt' @@ -79,55 +86,61 @@ def execute(): #pylint: disable=unused-variable init_rf_file.write(init_rf) iter_lmax_option = ' -lmax 4' else: - rf_in_path = 'iter' + str(iteration-1) + '_RF.txt' - mask_in_path = 'iter' + str(iteration-1) + '_SF_dilated.mif' + rf_in_path = f'iter{iteration-1}_RF.txt' + mask_in_path = f'iter{iteration-1}_SF_dilated.mif' iter_lmax_option = lmax_option # Run CSD - run.command('dwi2fod csd dwi.mif ' + rf_in_path + ' ' + prefix + 'FOD.mif -mask ' + mask_in_path) + run.command(f'dwi2fod csd dwi.mif {rf_in_path} {prefix}FOD.mif -mask {mask_in_path}') # Get amplitudes of two largest peaks, and direction of largest - run.command('fod2fixel ' + prefix + 'FOD.mif ' + prefix + 'fixel -peak_amp peak_amps.mif -mask ' + mask_in_path + ' -fmls_no_thresholds') - app.cleanup(prefix + 'FOD.mif') + run.command(f'fod2fixel {prefix}FOD.mif {prefix}fixel -peak_amp peak_amps.mif -mask {mask_in_path} -fmls_no_thresholds') + app.cleanup(f'{prefix}FOD.mif') if iteration: app.cleanup(mask_in_path) - run.command('fixel2voxel ' + os.path.join(prefix + 'fixel', 'peak_amps.mif') + ' none ' + prefix + 'amps.mif -number 2') - run.command('mrconvert ' + prefix + 'amps.mif ' + prefix + 'first_peaks.mif -coord 3 0 -axes 0,1,2') - run.command('mrconvert ' + prefix + 'amps.mif ' + prefix + 'second_peaks.mif -coord 3 1 -axes 0,1,2') - app.cleanup(prefix + 'amps.mif') - run.command('fixel2peaks ' + os.path.join(prefix + 'fixel', 'directions.mif') + ' ' + prefix + 'first_dir.mif -number 1') - app.cleanup(prefix + 'fixel') + run.command(['fixel2voxel', os.path.join(f'{prefix}fixel', 'peak_amps.mif'), 'none', f'{prefix}amps.mif', '-number', '2']) + run.command(f'mrconvert {prefix}amps.mif {prefix}first_peaks.mif -coord 3 0 -axes 0,1,2') + run.command(f'mrconvert {prefix}amps.mif {prefix}second_peaks.mif -coord 3 1 -axes 0,1,2') + app.cleanup(f'{prefix}amps.mif') + run.command(['fixel2peaks', os.path.join(f'{prefix}fixel', 'directions.mif'), f'{prefix}first_dir.mif', '-number', '1']) + app.cleanup(f'{prefix}fixel') # Calculate the 'cost function' Donald derived for selecting single-fibre voxels # https://github.com/MRtrix3/mrtrix3/pull/426 # sqrt(|peak1|) * (1 - |peak2| / |peak1|)^2 - run.command('mrcalc ' + prefix + 'first_peaks.mif -sqrt 1 ' + prefix + 'second_peaks.mif ' + prefix + 'first_peaks.mif -div -sub 2 -pow -mult '+ prefix + 'CF.mif') - app.cleanup(prefix + 'first_peaks.mif') - app.cleanup(prefix + 'second_peaks.mif') - voxel_count = image.statistics(prefix + 'CF.mif').count + run.command(f'mrcalc {prefix}first_peaks.mif -sqrt 1 {prefix}second_peaks.mif {prefix}first_peaks.mif -div -sub 2 -pow -mult {prefix}CF.mif') + app.cleanup(f'{prefix}first_peaks.mif') + app.cleanup(f'{prefix}second_peaks.mif') + voxel_count = image.statistics(f'{prefix}CF.mif').count # Select the top-ranked voxels - run.command('mrthreshold ' + prefix + 'CF.mif -top ' + str(min([app.ARGS.number, voxel_count])) + ' ' + prefix + 'SF.mif') + run.command(f'mrthreshold {prefix}CF.mif -top {min([app.ARGS.number, voxel_count])} {prefix}SF.mif') # Generate a new response function based on this selection - run.command('amp2response dwi.mif ' + prefix + 'SF.mif ' + prefix + 'first_dir.mif ' + prefix + 'RF.txt' + iter_lmax_option) - app.cleanup(prefix + 'first_dir.mif') + run.command(f'amp2response dwi.mif {prefix}SF.mif {prefix}first_dir.mif {prefix}RF.txt {iter_lmax_option}') + app.cleanup(f'{prefix}first_dir.mif') + + new_rf = matrix.load_vector(f'{prefix}RF.txt') + rf_string = ', '.join(f'{n:.3f}' for n in new_rf) + progress.increment('Optimising ' + f'({iteration+1} iterations, ' + f'RF: [ {rf_string} ])') - new_rf = matrix.load_vector(prefix + 'RF.txt') - progress.increment('Optimising (' + str(iteration+1) + ' iterations, RF: [ ' + ', '.join('{:.3f}'.format(n) for n in new_rf) + '] )') # Should we terminate? if iteration > 0: - run.command('mrcalc ' + prefix + 'SF.mif iter' + str(iteration-1) + '_SF.mif -sub ' + prefix + 'SF_diff.mif') - app.cleanup('iter' + str(iteration-1) + '_SF.mif') - max_diff = image.statistics(prefix + 'SF_diff.mif').max - app.cleanup(prefix + 'SF_diff.mif') + run.command(f'mrcalc {prefix}SF.mif iter{iteration-1}_SF.mif -sub {prefix}SF_diff.mif') + app.cleanup(f'iter{iteration-1}_SF.mif') + max_diff = image.statistics(f'{prefix}SF_diff.mif').max + app.cleanup(f'{prefix}SF_diff.mif') if not max_diff: - app.cleanup(prefix + 'CF.mif') - run.function(shutil.copyfile, prefix + 'RF.txt', 'response.txt') - run.function(shutil.move, prefix + 'SF.mif', 'voxels.mif') + app.cleanup(f'{prefix}CF.mif') + run.function(shutil.copyfile, f'{prefix}RF.txt', 'response.txt') + run.function(shutil.move, f'{prefix}SF.mif', 'voxels.mif') break # Select a greater number of top single-fibre voxels, and dilate (within bounds of initial mask); # these are the voxels that will be re-tested in the next iteration - run.command('mrthreshold ' + prefix + 'CF.mif -top ' + str(min([iter_voxels, voxel_count])) + ' - | maskfilter - dilate - -npass ' + str(app.ARGS.dilate) + ' | mrcalc mask.mif - -mult ' + prefix + 'SF_dilated.mif') - app.cleanup(prefix + 'CF.mif') + run.command(f'mrthreshold {prefix}CF.mif -top {min([iter_voxels, voxel_count])} - | ' + f'maskfilter - dilate - -npass {app.ARGS.dilate} | ' + f'mrcalc mask.mif - -mult {prefix}SF_dilated.mif') + app.cleanup(f'{prefix}CF.mif') iteration += 1 @@ -135,15 +148,15 @@ def execute(): #pylint: disable=unused-variable # If terminating due to running out of iterations, still need to put the results in the appropriate location if os.path.exists('response.txt'): - app.console('Convergence of SF voxel selection detected at iteration ' + str(iteration+1)) + app.console(f'Convergence of SF voxel selection detected at iteration {iteration+1}') else: - app.console('Exiting after maximum ' + str(app.ARGS.max_iters) + ' iterations') - run.function(shutil.copyfile, 'iter' + str(app.ARGS.max_iters-1) + '_RF.txt', 'response.txt') - run.function(shutil.move, 'iter' + str(app.ARGS.max_iters-1) + '_SF.mif', 'voxels.mif') + app.console(f'Exiting after maximum {app.ARGS.max_iters} iterations') + run.function(shutil.copyfile, f'iter{app.ARGS.max_iters-1}_RF.txt', 'response.txt') + run.function(shutil.move, f'iter{app.ARGS.max_iters-1}_SF.mif', 'voxels.mif') - run.function(shutil.copyfile, 'response.txt', path.from_user(app.ARGS.output, False)) + run.function(shutil.copyfile, 'response.txt', app.ARGS.output) if app.ARGS.voxels: - run.command('mrconvert voxels.mif ' + path.from_user(app.ARGS.voxels), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'voxels.mif', app.ARGS.voxels], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwibiascorrect/ants.py b/lib/mrtrix3/dwibiascorrect/ants.py index 08928a88d6..790a80da76 100644 --- a/lib/mrtrix3/dwibiascorrect/ants.py +++ b/lib/mrtrix3/dwibiascorrect/ants.py @@ -15,7 +15,7 @@ import shutil from mrtrix3 import MRtrixError -from mrtrix3 import app, path, run +from mrtrix3 import app, run @@ -30,38 +30,39 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('ants', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Perform DWI bias field correction using the N4 algorithm as provided in ANTs') - parser.add_citation('Tustison, N.; Avants, B.; Cook, P.; Zheng, Y.; Egan, A.; Yushkevich, P. & Gee, J. N4ITK: Improved N3 Bias Correction. IEEE Transactions on Medical Imaging, 2010, 29, 1310-1320', is_external=True) + parser.add_citation('Tustison, N.; Avants, B.; Cook, P.; Zheng, Y.; Egan, A.; Yushkevich, P. & Gee, J. ' + 'N4ITK: Improved N3 Bias Correction. ' + 'IEEE Transactions on Medical Imaging, 2010, 29, 1310-1320', + is_external=True) ants_options = parser.add_argument_group('Options for ANTs N4BiasFieldCorrection command') for key in sorted(OPT_N4_BIAS_FIELD_CORRECTION): - ants_options.add_argument('-ants_'+key, metavar=OPT_N4_BIAS_FIELD_CORRECTION[key][0], help='N4BiasFieldCorrection option -%s: %s' % (key,OPT_N4_BIAS_FIELD_CORRECTION[key][1])) - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input image series to be corrected') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') - - - -def check_output_paths(): #pylint: disable=unused-variable - pass - - - -def get_inputs(): #pylint: disable=unused-variable - pass + ants_options.add_argument(f'-ants_{key}', + metavar=OPT_N4_BIAS_FIELD_CORRECTION[key][0], + help=f'N4BiasFieldCorrection option -{key}: {OPT_N4_BIAS_FIELD_CORRECTION[key][1]}') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input image series to be corrected') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output corrected image series') def execute(): #pylint: disable=unused-variable if not shutil.which('N4BiasFieldCorrection'): - raise MRtrixError('Could not find ANTS program N4BiasFieldCorrection; please check installation') + raise MRtrixError('Could not find ANTS program N4BiasFieldCorrection; ' + 'please check installation') for key in sorted(OPT_N4_BIAS_FIELD_CORRECTION): - if hasattr(app.ARGS, 'ants_' + key): - val = getattr(app.ARGS, 'ants_' + key) + if hasattr(app.ARGS, f'ants_{key}'): + val = getattr(app.ARGS, f'ants_{key}') if val is not None: OPT_N4_BIAS_FIELD_CORRECTION[key] = (val, 'user defined') - ants_options = ' '.join(['-%s %s' %(k, v[0]) for k, v in OPT_N4_BIAS_FIELD_CORRECTION.items()]) + ants_options = ' '.join([f'-{k} {v[0]}' for k, v in OPT_N4_BIAS_FIELD_CORRECTION.items()]) # Generate a mean b=0 image - run.command('dwiextract in.mif - -bzero | mrmath - mean mean_bzero.mif -axis 3') + run.command('dwiextract in.mif - -bzero | ' + 'mrmath - mean mean_bzero.mif -axis 3') # Use the brain mask as a weights image rather than a mask; means that voxels at the edge of the mask # will have a smoothly-varying bias field correction applied, rather than multiplying by 1.0 outside the mask @@ -69,24 +70,32 @@ def execute(): #pylint: disable=unused-variable run.command('mrconvert mask.mif mask.nii -strides +1,+2,+3') init_bias_path = 'init_bias.nii' corrected_path = 'corrected.nii' - run.command('N4BiasFieldCorrection -d 3 -i mean_bzero.nii -w mask.nii -o [' + corrected_path + ',' + init_bias_path + '] ' + ants_options) + run.command(f'N4BiasFieldCorrection -d 3 -i mean_bzero.nii -w mask.nii -o [{corrected_path},{init_bias_path}] {ants_options}') # N4 can introduce large differences between subjects via a global scaling of the bias field # Estimate this scaling based on the total integral of the pre- and post-correction images within the brain mask - input_integral = float(run.command('mrcalc mean_bzero.mif mask.mif -mult - | mrmath - sum - -axis 0 | mrmath - sum - -axis 1 | mrmath - sum - -axis 2 | mrdump -').stdout) - output_integral = float(run.command('mrcalc ' + corrected_path + ' mask.mif -mult - | mrmath - sum - -axis 0 | mrmath - sum - -axis 1 | mrmath - sum - -axis 2 | mrdump -').stdout) + input_integral = float(run.command('mrcalc mean_bzero.mif mask.mif -mult - | ' + 'mrmath - sum - -axis 0 | ' + 'mrmath - sum - -axis 1 | ' + 'mrmath - sum - -axis 2 | ' + 'mrdump -').stdout) + output_integral = float(run.command(f'mrcalc {corrected_path} mask.mif -mult - | ' + 'mrmath - sum - -axis 0 | ' + 'mrmath - sum - -axis 1 | ' + 'mrmath - sum - -axis 2 | ' + 'mrdump -').stdout) multiplier = output_integral / input_integral - app.debug('Integrals: Input = ' + str(input_integral) + '; Output = ' + str(output_integral) + '; resulting multiplier = ' + str(multiplier)) - run.command('mrcalc ' + init_bias_path + ' ' + str(multiplier) + ' -mult bias.mif') + app.debug(f'Integrals: Input = {input_integral}; Output = {output_integral}; resulting multiplier = {multiplier}') + run.command(f'mrcalc {init_bias_path} {multiplier} -mult bias.mif') # Common final steps for all algorithms run.command('mrcalc in.mif bias.mif -div result.mif') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) if app.ARGS.bias: - run.command('mrconvert bias.mif ' + path.from_user(app.ARGS.bias), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'bias.mif', app.ARGS.bias], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwibiascorrect/fsl.py b/lib/mrtrix3/dwibiascorrect/fsl.py index dd78ad7335..7e335d663d 100644 --- a/lib/mrtrix3/dwibiascorrect/fsl.py +++ b/lib/mrtrix3/dwibiascorrect/fsl.py @@ -15,29 +15,34 @@ import os from mrtrix3 import MRtrixError -from mrtrix3 import app, fsl, path, run, utils +from mrtrix3 import app, fsl, run, utils def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('fsl', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - parser.set_synopsis('Perform DWI bias field correction using the \'fast\' command as provided in FSL') - parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. IEEE Transactions on Medical Imaging, 2001, 20, 45-57', is_external=True) - parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. Advances in functional and structural MR image analysis and implementation as FSL. NeuroImage, 2004, 23, S208-S219', is_external=True) - parser.add_description('The FSL \'fast\' command only estimates the bias field within a brain mask, and cannot extrapolate this smoothly-varying field beyond the defined mask. As such, this algorithm by necessity introduces a hard masking of the input DWI. Since this attribute may interfere with the purpose of using the command (e.g. correction of a bias field is commonly used to improve brain mask estimation), use of this particular algorithm is generally not recommended.') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input image series to be corrected') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') - - - -def check_output_paths(): #pylint: disable=unused-variable - pass - - - -def get_inputs(): #pylint: disable=unused-variable - pass + parser.set_synopsis('Perform DWI bias field correction using the "fast" command as provided in FSL') + parser.add_citation('Zhang, Y.; Brady, M. & Smith, S. ' + 'Segmentation of brain MR images through a hidden Markov random field model and the expectation-maximization algorithm. ' + 'IEEE Transactions on Medical Imaging, 2001, 20, 45-57', + is_external=True) + parser.add_citation('Smith, S. M.; Jenkinson, M.; Woolrich, M. W.; Beckmann, C. F.; Behrens, T. E.; Johansen-Berg, H.; Bannister, P. R.; De Luca, M.; Drobnjak, I.; Flitney, D. E.; Niazy, R. K.; Saunders, J.; Vickers, J.; Zhang, Y.; De Stefano, N.; Brady, J. M. & Matthews, P. M. ' + 'Advances in functional and structural MR image analysis and implementation as FSL. ' + 'NeuroImage, 2004, 23, S208-S219', + is_external=True) + parser.add_description('The FSL "fast" command only estimates the bias field within a brain mask, ' + 'and cannot extrapolate this smoothly-varying field beyond the defined mask. ' + 'As such, this algorithm by necessity introduces a hard masking of the input DWI. ' + 'Since this attribute may interfere with the purpose of using the command ' + '(e.g. correction of a bias field is commonly used to improve brain mask estimation), ' + 'use of this particular algorithm is generally not recommended.') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input image series to be corrected') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output corrected image series') @@ -46,7 +51,8 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Script cannot run using FSL on Windows due to FSL dependency') if not os.environ.get('FSLDIR', ''): - raise MRtrixError('Environment variable FSLDIR is not set; please run appropriate FSL configuration script') + raise MRtrixError('Environment variable FSLDIR is not set; ' + 'please run appropriate FSL configuration script') fast_cmd = fsl.exe_name('fast') @@ -55,22 +61,24 @@ def execute(): #pylint: disable=unused-variable 'Use of the ants algorithm is recommended for quantitative DWI analyses.') # Generate a mean b=0 image - run.command('dwiextract in.mif - -bzero | mrmath - mean mean_bzero.mif -axis 3') + run.command('dwiextract in.mif - -bzero | ' + 'mrmath - mean mean_bzero.mif -axis 3') # FAST doesn't accept a mask input; therefore need to explicitly mask the input image - run.command('mrcalc mean_bzero.mif mask.mif -mult - | mrconvert - mean_bzero_masked.nii -strides -1,+2,+3') - run.command(fast_cmd + ' -t 2 -o fast -n 3 -b mean_bzero_masked.nii') + run.command('mrcalc mean_bzero.mif mask.mif -mult - | ' + 'mrconvert - mean_bzero_masked.nii -strides -1,+2,+3') + run.command(f'{fast_cmd} -t 2 -o fast -n 3 -b mean_bzero_masked.nii') bias_path = fsl.find_image('fast_bias') # Rather than using a bias field estimate of 1.0 outside the brain mask, zero-fill the # output image outside of this mask - run.command('mrcalc in.mif ' + bias_path + ' -div mask.mif -mult result.mif') - run.command('mrconvert result.mif ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(f'mrcalc in.mif {bias_path} -div mask.mif -mult result.mif') + run.command(['mrconvert', 'result.mif', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) if app.ARGS.bias: - run.command('mrconvert ' + bias_path + ' ' + path.from_user(app.ARGS.bias), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', bias_path, app.ARGS.bias], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwibiascorrect/mtnorm.py b/lib/mrtrix3/dwibiascorrect/mtnorm.py index 5a7276a068..acf4903bf9 100644 --- a/lib/mrtrix3/dwibiascorrect/mtnorm.py +++ b/lib/mrtrix3/dwibiascorrect/mtnorm.py @@ -14,7 +14,7 @@ # For more details, see http://www.mrtrix.org/. from mrtrix3 import MRtrixError -from mrtrix3 import app, image, path, run +from mrtrix3 import app, image, run LMAXES_MULTI = [4, 0, 0] @@ -22,19 +22,23 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mtnorm', parents=[base_parser]) - parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') + parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' + 'and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') parser.set_synopsis('Perform DWI bias field correction using the "mtnormalise" command') parser.add_description('This algorithm bases its operation almost entirely on the utilisation of multi-tissue ' - 'decomposition information to estimate an underlying B1 receive field, as is implemented ' - 'in the MRtrix3 command "mtnormalise". Its typical usage is however slightly different, ' - 'in that the primary output of the command is not the bias-field-corrected FODs, but a ' - 'bias-field-corrected version of the DWI series.') + 'decomposition information to estimate an underlying B1 receive field, ' + 'as is implemented in the MRtrix3 command "mtnormalise". ' + 'Its typical usage is however slightly different, ' + 'in that the primary output of the command is not the bias-field-corrected FODs, ' + 'but a bias-field-corrected version of the DWI series.') parser.add_description('The operation of this script is a subset of that performed by the script "dwibiasnormmask". ' - 'Many users may find that comprehensive solution preferable; this dwibiascorrect algorithm is ' - 'nevertheless provided to demonstrate specifically the bias field correction portion of that command.') - parser.add_description('The ODFs estimated within this optimisation procedure are by default of lower maximal spherical harmonic ' - 'degree than what would be advised for analysis. This is done for computational efficiency. This ' - 'behaviour can be modified through the -lmax command-line option.') + 'Many users may find that comprehensive solution preferable; ' + 'this dwibiascorrect algorithm is nevertheless provided to demonstrate ' + 'specifically the bias field correction portion of that command.') + parser.add_description('The ODFs estimated within this optimisation procedure are by default of lower maximal ' + 'spherical harmonic degree than what would be advised for analysis. ' + 'This is done for computational efficiency. ' + 'This behaviour can be modified through the -lmax command-line option.') parser.add_citation('Jeurissen, B; Tournier, J-D; Dhollander, T; Connelly, A & Sijbers, J. ' 'Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. ' 'NeuroImage, 2014, 103, 411-426') @@ -44,24 +48,19 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Tabbara, R.; Rosnarho-Tornstrand, J.; Tournier, J.-D.; Raffelt, D. & Connelly, A. ' 'Multi-tissue log-domain intensity and inhomogeneity normalisation for quantitative apparent fibre density. ' 'In Proc. ISMRM, 2021, 29, 2472') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input image series to be corrected') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The output corrected image series') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input image series to be corrected') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The output corrected image series') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', type=app.Parser.SequenceInt(), metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' - 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') - - - -def check_output_paths(): #pylint: disable=unused-variable - pass - - - -def get_inputs(): #pylint: disable=unused-variable - pass + f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' + f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data)') @@ -70,10 +69,7 @@ def execute(): #pylint: disable=unused-variable # Verify user inputs lmax = None if app.ARGS.lmax: - try: - lmax = [int(i) for i in app.ARGS.lmax.split(',')] - except ValueError as exc: - raise MRtrixError('Values provided to -lmax option must be a comma-separated list of integers') from exc + lmax = app.ARGS.lmax if any(value < 0 or value % 2 for value in lmax): raise MRtrixError('lmax values must be non-negative even integers') if len(lmax) not in [2, 3]: @@ -88,49 +84,48 @@ def execute(): #pylint: disable=unused-variable if lmax is None: lmax = LMAXES_MULTI if multishell else LMAXES_SINGLE elif len(lmax) == 3 and not multishell: - raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, but input DWI is not multi-shell') + raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, ' + 'but input DWI is not multi-shell') # RF estimation and multi-tissue CSD class Tissue(object): #pylint: disable=useless-object-inheritance def __init__(self, name): self.name = name - self.tissue_rf = 'response_' + name + '.txt' - self.fod = 'FOD_' + name + '.mif' - self.fod_norm = 'FODnorm_' + name + '.mif' + self.rffile = f'response_{name}.txt' + self.fod = f'FOD_{name}.mif' + self.fod_norm = f'FODnorm_{name}.mif' tissues = [Tissue('WM'), Tissue('GM'), Tissue('CSF')] - run.command('dwi2response dhollander in.mif' - + (' -mask mask.mif' if app.ARGS.mask else '') - + ' ' - + ' '.join(tissue.tissue_rf for tissue in tissues)) + # TODO Fix + run.command(['dwi2response', 'dhollander', 'in.mif', [tissue.rffile for tissue in tissues]] + .extend(['-mask', 'mask.mif'] if app.ARGS.mask else [])) # Immediately remove GM if we can't deal with it if not multishell: - app.cleanup(tissues[1].tissue_rf) + app.cleanup(tissues[1].rffile) tissues = tissues[::2] run.command('dwi2fod msmt_csd in.mif' - + ' -lmax ' + ','.join(str(item) for item in lmax) + + ' -lmax ' + ','.join(map(str, lmax)) + ' ' - + ' '.join(tissue.tissue_rf + ' ' + tissue.fod + + ' '.join(f'{tissue.rffile} {tissue.fod}' for tissue in tissues)) run.command('maskfilter mask.mif erode - | ' - + 'mtnormalise -mask - -balanced' - + ' -check_norm field.mif ' - + ' '.join(tissue.fod + ' ' + tissue.fod_norm - for tissue in tissues)) + 'mtnormalise -mask - -balanced -check_norm field.mif ' + + ' '.join(f'{tissue.fod} {tissue.fod_norm}' + for tissue in tissues)) app.cleanup([tissue.fod for tissue in tissues]) app.cleanup([tissue.fod_norm for tissue in tissues]) - app.cleanup([tissue.tissue_rf for tissue in tissues]) + app.cleanup([tissue.rffile for tissue in tissues]) - run.command('mrcalc in.mif field.mif -div - | ' - 'mrconvert - '+ path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrcalc', 'in.mif', 'field.mif', '-div', '-', '|', + 'mrconvert', '-', app.ARGS.output], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) if app.ARGS.bias: - run.command('mrconvert field.mif ' + path.from_user(app.ARGS.bias), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(['mrconvert', 'field.mif', app.ARGS.bias], + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) diff --git a/lib/mrtrix3/dwinormalise/group.py b/lib/mrtrix3/dwinormalise/group.py index 7be4759902..1fd8901085 100644 --- a/lib/mrtrix3/dwinormalise/group.py +++ b/lib/mrtrix3/dwinormalise/group.py @@ -13,7 +13,7 @@ # # For more details, see http://www.mrtrix.org/. -import os, shlex +import os from mrtrix3 import MRtrixError from mrtrix3 import app, image, path, run, utils @@ -24,22 +24,40 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('group', parents=[base_parser]) parser.set_author('David Raffelt (david.raffelt@florey.edu.au)') - parser.set_synopsis('Performs a global DWI intensity normalisation on a group of subjects using the median b=0 white matter value as the reference') - parser.add_description('The white matter mask is estimated from a population average FA template then warped back to each subject to perform the intensity normalisation. Note that bias field correction should be performed prior to this step.') - parser.add_description('All input DWI files must contain an embedded diffusion gradient table; for this reason, these images must all be in either .mif or .mif.gz format.') - parser.add_argument('input_dir', type=app.Parser.DirectoryIn(), help='The input directory containing all DWI images') - parser.add_argument('mask_dir', type=app.Parser.DirectoryIn(), help='Input directory containing brain masks, corresponding to one per input image (with the same file name prefix)') - parser.add_argument('output_dir', type=app.Parser.DirectoryOut(), help='The output directory containing all of the intensity normalised DWI images') - parser.add_argument('fa_template', type=app.Parser.ImageOut(), help='The output population-specific FA template, which is thresholded to estimate a white matter mask') - parser.add_argument('wm_mask', type=app.Parser.ImageOut(), help='The output white matter mask (in template space), used to estimate the median b=0 white matter value for normalisation') - parser.add_argument('-fa_threshold', type=app.Parser.Float(0.0, 1.0), default=FA_THRESHOLD_DEFAULT, metavar='value', help='The threshold applied to the Fractional Anisotropy group template used to derive an approximate white matter mask (default: ' + str(FA_THRESHOLD_DEFAULT) + ')') - - - -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output_dir) - app.check_output_path(app.ARGS.fa_template) - app.check_output_path(app.ARGS.wm_mask) + parser.set_synopsis('Performs a global DWI intensity normalisation on a group of subjects ' + 'using the median b=0 white matter value as the reference') + parser.add_description('The white matter mask is estimated from a population average FA template ' + 'then warped back to each subject to perform the intensity normalisation. ' + 'Note that bias field correction should be performed prior to this step.') + parser.add_description('All input DWI files must contain an embedded diffusion gradient table; ' + 'for this reason, ' + 'these images must all be in either .mif or .mif.gz format.') + parser.add_argument('input_dir', + type=app.Parser.DirectoryIn(), + help='The input directory containing all DWI images') + parser.add_argument('mask_dir', + type=app.Parser.DirectoryIn(), + help='Input directory containing brain masks, ' + 'corresponding to one per input image ' + '(with the same file name prefix)') + parser.add_argument('output_dir', + type=app.Parser.DirectoryOut(), + help='The output directory containing all of the intensity normalised DWI images') + parser.add_argument('fa_template', + type=app.Parser.ImageOut(), + help='The output population-specific FA template, ' + 'which is thresholded to estimate a white matter mask') + parser.add_argument('wm_mask', + type=app.Parser.ImageOut(), + help='The output white matter mask (in template space), ' + 'used to estimate the median b=0 white matter value for normalisation') + parser.add_argument('-fa_threshold', + type=app.Parser.Float(0.0, 1.0), + default=FA_THRESHOLD_DEFAULT, + metavar='value', + help='The threshold applied to the Fractional Anisotropy group template ' + 'used to derive an approximate white matter mask ' + f'(default: {FA_THRESHOLD_DEFAULT})') @@ -52,21 +70,17 @@ def __init__(self, filename, prefix, mask_filename = ''): self.mask_filename = mask_filename - input_dir = path.from_user(app.ARGS.input_dir, False) - if not os.path.exists(input_dir): - raise MRtrixError('input directory not found') - in_files = path.all_in_dir(input_dir, dir_path=False) + in_files = path.all_in_dir(app.ARGS.input_dir, dir_path=False) if len(in_files) <= 1: - raise MRtrixError('not enough images found in input directory: more than one image is needed to perform a group-wise intensity normalisation') + raise MRtrixError('not enough images found in input directory: ' + 'more than one image is needed to perform a group-wise intensity normalisation') - app.console('performing global intensity normalisation on ' + str(len(in_files)) + ' input images') + app.console(f'Performing global intensity normalisation on {len(in_files)} input images') - mask_dir = path.from_user(app.ARGS.mask_dir, False) - if not os.path.exists(mask_dir): - raise MRtrixError('mask directory not found') - mask_files = path.all_in_dir(mask_dir, dir_path=False) + mask_files = path.all_in_dir(app.ARGS.mask_dir, dir_path=False) if len(mask_files) != len(in_files): - raise MRtrixError('the number of images in the mask directory does not equal the number of images in the input directory') + raise MRtrixError('the number of images in the mask directory does not equal the ' + 'number of images in the input directory') mask_common_postfix = os.path.commonprefix([i[::-1] for i in mask_files])[::-1] mask_prefixes = [] for mask_file in mask_files: @@ -77,48 +91,61 @@ def __init__(self, filename, prefix, mask_filename = ''): for i in in_files: subj_prefix = i.split(common_postfix)[0] if subj_prefix not in mask_prefixes: - raise MRtrixError ('no matching mask image was found for input image ' + i) - image.check_3d_nonunity(os.path.join(input_dir, i)) + raise MRtrixError (f'no matching mask image was found for input image {i}') + image.check_3d_nonunity(os.path.join(app.ARGS.input_dir, i)) index = mask_prefixes.index(subj_prefix) input_list.append(Input(i, subj_prefix, mask_files[index])) - app.make_scratch_dir() - app.goto_scratch_dir() + app.activate_scratch_dir() utils.make_dir('fa') progress = app.ProgressBar('Computing FA images', len(input_list)) for i in input_list: - run.command('dwi2tensor ' + shlex.quote(os.path.join(input_dir, i.filename)) + ' -mask ' + shlex.quote(os.path.join(mask_dir, i.mask_filename)) + ' - | tensor2metric - -fa ' + os.path.join('fa', i.prefix + '.mif')) + run.command(['dwi2tensor', os.path.join(app.ARGS.input_dir, i.filename), '-mask', os.path.join(app.ARGS.mask_dir, i.mask_filename), '-', '|', + 'tensor2metric', '-', '-fa', os.path.join('fa', f'{i.prefix}.mif')]) progress.increment() progress.done() app.console('Generating FA population template') - run.command('population_template fa fa_template.mif' - + ' -mask_dir ' + mask_dir - + ' -type rigid_affine_nonlinear' - + ' -rigid_scale 0.25,0.5,0.8,1.0' - + ' -affine_scale 0.7,0.8,1.0,1.0' - + ' -nl_scale 0.5,0.75,1.0,1.0,1.0' - + ' -nl_niter 5,5,5,5,5' - + ' -warp_dir warps' - + ' -linear_no_pause' - + ' -scratch population_template' - + ('' if app.DO_CLEANUP else ' -nocleanup')) + run.command(['population_template', 'fa', 'fa_template.mif', + '-mask_dir', app.ARGS.mask_dir, + '-type', 'rigid_affine_nonlinear', + '-rigid_scale', '0.25,0.5,0.8,1.0', + '-affine_scale', '0.7,0.8,1.0,1.0', + '-nl_scale', '0.5,0.75,1.0,1.0,1.0', + '-nl_niter', '5,5,5,5,5', + '-warp_dir', 'warps', + '-linear_no_pause', + '-scratch', 'population_template'] + + ([] if app.DO_CLEANUP else ['-nocleanup'])) app.console('Generating WM mask in template space') - run.command('mrthreshold fa_template.mif -abs ' + str(app.ARGS.fa_threshold) + ' template_wm_mask.mif') + run.command(f'mrthreshold fa_template.mif -abs {app.ARGS.fa_threshold} template_wm_mask.mif') progress = app.ProgressBar('Intensity normalising subject images', len(input_list)) - utils.make_dir(path.from_user(app.ARGS.output_dir, False)) + utils.make_dir(app.ARGS.output_dir) utils.make_dir('wm_mask_warped') for i in input_list: - run.command('mrtransform template_wm_mask.mif -interp nearest -warp_full ' + os.path.join('warps', i.prefix + '.mif') + ' ' + os.path.join('wm_mask_warped', i.prefix + '.mif') + ' -from 2 -template ' + os.path.join('fa', i.prefix + '.mif')) - run.command('dwinormalise manual ' + shlex.quote(os.path.join(input_dir, i.filename)) + ' ' + os.path.join('wm_mask_warped', i.prefix + '.mif') + ' temp.mif') - run.command('mrconvert temp.mif ' + path.from_user(os.path.join(app.ARGS.output_dir, i.filename)), mrconvert_keyval=path.from_user(os.path.join(input_dir, i.filename), False), force=app.FORCE_OVERWRITE) + run.command(['mrtransform', 'template_wm_mask.mif', os.path.join('wm_mask_warped', f'{i.prefix}.mif'), + '-interp', 'nearest', + '-warp_full', os.path.join('warps', f'{i.prefix}.mif'), + '-from', '2', + '-template', os.path.join('fa', f'{i.prefix}.mif')]) + run.command(['dwinormalise', 'manual', + os.path.join(app.ARGS.input_dir, i.filename), + os.path.join('wm_mask_warped', f'{i.prefix}.mif'), + 'temp.mif']) + run.command(['mrconvert', 'temp.mif', os.path.join(app.ARGS.output_dir, i.filename)], + mrconvert_keyval=os.path.join(app.ARGS.input_dir, i.filename), + force=app.FORCE_OVERWRITE) os.remove('temp.mif') progress.increment() progress.done() app.console('Exporting template images to user locations') - run.command('mrconvert template_wm_mask.mif ' + path.from_user(app.ARGS.wm_mask), mrconvert_keyval='NULL', force=app.FORCE_OVERWRITE) - run.command('mrconvert fa_template.mif ' + path.from_user(app.ARGS.fa_template), mrconvert_keyval='NULL', force=app.FORCE_OVERWRITE) + run.command(['mrconvert', 'template_wm_mask.mif', app.ARGS.wm_mask], + mrconvert_keyval='NULL', + force=app.FORCE_OVERWRITE) + run.command(['mrconvert', 'fa_template.mif', app.ARGS.fa_template], + mrconvert_keyval='NULL', + force=app.FORCE_OVERWRITE) diff --git a/lib/mrtrix3/dwinormalise/manual.py b/lib/mrtrix3/dwinormalise/manual.py index 61dffd009c..7e7dc84a8d 100644 --- a/lib/mrtrix3/dwinormalise/manual.py +++ b/lib/mrtrix3/dwinormalise/manual.py @@ -14,7 +14,7 @@ # For more details, see http://www.mrtrix.org/. import math -from mrtrix3 import app, path, run +from mrtrix3 import app, run DEFAULT_TARGET_INTENSITY=1000 @@ -23,34 +23,47 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('manual', parents=[base_parser]) - parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') + parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' + 'and David Raffelt (david.raffelt@florey.edu.au)') parser.set_synopsis('Intensity normalise a DWI series based on the b=0 signal within a supplied mask') - parser.add_argument('input_dwi', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('input_mask', type=app.Parser.ImageIn(), help='The mask within which a reference b=0 intensity will be sampled') - parser.add_argument('output_dwi', type=app.Parser.ImageOut(), help='The output intensity-normalised DWI series') - parser.add_argument('-intensity', type=app.Parser.Float(0.0), metavar='value', default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value (Default: ' + str(DEFAULT_TARGET_INTENSITY) + ')') - parser.add_argument('-percentile', type=app.Parser.Float(0.0, 100.0), metavar='value', help='Define the percentile of the b=0 image intensties within the mask used for normalisation; if this option is not supplied then the median value (50th percentile) will be normalised to the desired intensity value') + parser.add_argument('input_dwi', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('input_mask', + type=app.Parser.ImageIn(), + help='The mask within which a reference b=0 intensity will be sampled') + parser.add_argument('output_dwi', + type=app.Parser.ImageOut(), + help='The output intensity-normalised DWI series') + parser.add_argument('-intensity', + type=app.Parser.Float(0.0), + metavar='value', + default=DEFAULT_TARGET_INTENSITY, + help='Normalise the b=0 signal to a specified value ' + f'(Default: {DEFAULT_TARGET_INTENSITY})') + parser.add_argument('-percentile', + type=app.Parser.Float(0.0, 100.0), + metavar='value', + help='Define the percentile of the b=0 image intensties within the mask used for normalisation; ' + 'if this option is not supplied then the median value (50th percentile) ' + 'will be normalised to the desired intensity value') app.add_dwgrad_import_options(parser) -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output_dwi) - - - def execute(): #pylint: disable=unused-variable grad_option = '' if app.ARGS.grad: - grad_option = ' -grad ' + path.from_user(app.ARGS.grad) + grad_option = f' -grad {app.ARGS.grad}' elif app.ARGS.fslgrad: - grad_option = ' -fslgrad ' + path.from_user(app.ARGS.fslgrad[0]) + ' ' + path.from_user(app.ARGS.fslgrad[1]) + grad_option = f' -fslgrad {app.ARGS.fslgrad[0]} {app.ARGS.fslgrad[1]}' if app.ARGS.percentile: - intensities = [float(value) for value in run.command('dwiextract ' + path.from_user(app.ARGS.input_dwi) + grad_option + ' -bzero - | ' + \ - 'mrmath - mean - -axis 3 | ' + \ - 'mrdump - -mask ' + path.from_user(app.ARGS.input_mask)).stdout.splitlines()] + intensities = [float(value) for value in run.command(f'dwiextract {app.ARGS.input_dwi} {grad_option} -bzero - | ' + f'mrmath - mean - -axis 3 | ' + f'mrdump - -mask {app.ARGS.input_mask}', + preserve_pipes=True).stdout.splitlines()] intensities = sorted(intensities) float_index = 0.01 * app.ARGS.percentile * len(intensities) lower_index = int(math.floor(float_index)) @@ -60,14 +73,14 @@ def execute(): #pylint: disable=unused-variable interp_mu = float_index - float(lower_index) reference_value = (1.0-interp_mu)*intensities[lower_index] + interp_mu*intensities[lower_index+1] else: - reference_value = float(run.command('dwiextract ' + path.from_user(app.ARGS.input_dwi) + grad_option + ' -bzero - | ' + \ - 'mrmath - mean - -axis 3 | ' + \ - 'mrstats - -mask ' + path.from_user(app.ARGS.input_mask) + ' -output median', + reference_value = float(run.command(f'dwiextract {app.ARGS.input_dwi} {grad_option} -bzero - | ' + f'mrmath - mean - -axis 3 | ' + f'mrstats - -mask {app.ARGS.input_mask} -output median', preserve_pipes=True).stdout) multiplier = app.ARGS.intensity / reference_value - run.command('mrcalc ' + path.from_user(app.ARGS.input_dwi) + ' ' + str(multiplier) + ' -mult - | ' + \ - 'mrconvert - ' + path.from_user(app.ARGS.output_dwi) + grad_option, \ - mrconvert_keyval=path.from_user(app.ARGS.input_dwi, False), \ + run.command(f'mrcalc {app.ARGS.input_dwi} {multiplier} -mult - | ' + f'mrconvert - {app.ARGS.output_dwi} {grad_option}', + mrconvert_keyval=app.ARGS.input_dwi, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwinormalise/mtnorm.py b/lib/mrtrix3/dwinormalise/mtnorm.py index 38f1a1f091..574152dd3e 100644 --- a/lib/mrtrix3/dwinormalise/mtnorm.py +++ b/lib/mrtrix3/dwinormalise/mtnorm.py @@ -15,7 +15,7 @@ import math from mrtrix3 import CONFIG, MRtrixError -from mrtrix3 import app, image, matrix, path, run +from mrtrix3 import app, image, matrix, run REFERENCE_INTENSITY = 1000 @@ -26,17 +26,21 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mtnorm', parents=[base_parser]) - parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') + parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' + 'and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') parser.set_synopsis('Normalise a DWI series to the estimated b=0 CSF intensity') parser.add_description('This algorithm determines an appropriate global scaling factor to apply to a DWI series ' - 'such that after the scaling is applied, the b=0 CSF intensity corresponds to some ' - 'reference value (' + str(REFERENCE_INTENSITY) + ' by default).') + 'such that after the scaling is applied, ' + 'the b=0 CSF intensity corresponds to some reference value ' + f'({REFERENCE_INTENSITY} by default).') parser.add_description('The operation of this script is a subset of that performed by the script "dwibiasnormmask". ' - 'Many users may find that comprehensive solution preferable; this dwinormalise algorithm is ' - 'nevertheless provided to demonstrate specifically the global intensituy normalisation portion of that command.') - parser.add_description('The ODFs estimated within this optimisation procedure are by default of lower maximal spherical harmonic ' - 'degree than what would be advised for analysis. This is done for computational efficiency. This ' - 'behaviour can be modified through the -lmax command-line option.') + 'Many users may find that comprehensive solution preferable; ' + 'this dwinormalise algorithm is nevertheless provided to demonstrate ' + 'specifically the global intensituy normalisation portion of that command.') + parser.add_description('The ODFs estimated within this optimisation procedure are by default of lower maximal ' + 'spherical harmonic degree than what would be advised for analysis. ' + 'This is done for computational efficiency. ' + 'This behaviour can be modified through the -lmax command-line option.') parser.add_citation('Jeurissen, B; Tournier, J-D; Dhollander, T; Connelly, A & Sijbers, J. ' 'Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. ' 'NeuroImage, 2014, 103, 411-426') @@ -49,14 +53,19 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') - parser.add_argument('input', type=app.Parser.ImageIn(), help='The input DWI series') - parser.add_argument('output', type=app.Parser.ImageOut(), help='The normalised DWI series') + parser.add_argument('input', + type=app.Parser.ImageIn(), + help='The input DWI series') + parser.add_argument('output', + type=app.Parser.ImageOut(), + help='The normalised DWI series') options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', type=app.Parser.SequenceInt(), metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' - 'defaults are "' + ','.join(str(item) for item in LMAXES_MULTI) + '" for multi-shell and "' + ','.join(str(item) for item in LMAXES_SINGLE) + '" for single-shell data)') + f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' + f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data)') options.add_argument('-mask', type=app.Parser.ImageIn(), metavar='image', @@ -67,7 +76,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable metavar='value', default=REFERENCE_INTENSITY, help='Set the target CSF b=0 intensity in the output DWI series ' - '(default: ' + str(REFERENCE_INTENSITY) + ')') + f'(default: {REFERENCE_INTENSITY})') options.add_argument('-scale', type=app.Parser.FileOut(), metavar='file', @@ -76,46 +85,30 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable -def check_output_paths(): #pylint: disable=unused-variable - app.check_output_path(app.ARGS.output) - - - def execute(): #pylint: disable=unused-variable # Verify user inputs lmax = None if app.ARGS.lmax: - try: - lmax = [int(i) for i in app.ARGS.lmax.split(',')] - except ValueError as exc: - raise MRtrixError('Values provided to -lmax option must be a comma-separated list of integers') from exc + lmax = app.ARGS.lmax if any(value < 0 or value % 2 for value in lmax): raise MRtrixError('lmax values must be non-negative even integers') if len(lmax) not in [2, 3]: raise MRtrixError('Length of lmax vector expected to be either 2 or 3') - if app.ARGS.reference <= 0.0: - raise MRtrixError('Reference intensity must be positive') - - grad_option = app.read_dwgrad_import_options() # Get input data into the scratch directory - app.make_scratch_dir() - run.command('mrconvert ' - + path.from_user(app.ARGS.input) - + ' ' - + path.to_scratch('input.mif') - + grad_option) + grad_option = app.read_dwgrad_import_options().split(' ') + app.activate_scratch_dir() + run.command(['mrconvert', app.ARGS.input, 'input.mif'] + grad_option) if app.ARGS.mask: - run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + ' -datatype bit') - app.goto_scratch_dir() + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit']) # Make sure we have a valid mask available if app.ARGS.mask: if not image.match('input.mif', 'mask.mif', up_to_dim=3): raise MRtrixError('Provided mask image does not match input DWI') else: - run.command('dwi2mask ' + CONFIG['Dwi2maskAlgorithm'] + ' input.mif mask.mif') + run.command(['dwi2mask', CONFIG['Dwi2maskAlgorithm'], 'input.mif', 'mask.mif']) # Determine whether we are working with single-shell or multi-shell data bvalues = [ @@ -126,37 +119,37 @@ def execute(): #pylint: disable=unused-variable if lmax is None: lmax = LMAXES_MULTI if multishell else LMAXES_SINGLE elif len(lmax) == 3 and not multishell: - raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, but input DWI is not multi-shell') + raise MRtrixError('User specified 3 lmax values for three-tissue decomposition, ' + 'but input DWI is not multi-shell') # RF estimation and multi-tissue CSD class Tissue(object): #pylint: disable=useless-object-inheritance def __init__(self, name): self.name = name - self.tissue_rf = 'response_' + name + '.txt' - self.fod = 'FOD_' + name + '.mif' - self.fod_norm = 'FODnorm_' + name + '.mif' + self.tissue_rf = f'response_{name}.txt' + self.fod = f'FOD_{name}.mif' + self.fod_norm = f'FODnorm_{name}.mif' tissues = [Tissue('WM'), Tissue('GM'), Tissue('CSF')] run.command('dwi2response dhollander input.mif -mask mask.mif ' - + ' '.join(tissue.tissue_rf for tissue in tissues)) + f'{" ".join(tissue.tissue_rf for tissue in tissues)}') # Immediately remove GM if we can't deal with it if not multishell: app.cleanup(tissues[1].tissue_rf) tissues = tissues[::2] - run.command('dwi2fod msmt_csd input.mif' - + ' -lmax ' + ','.join(str(item) for item in lmax) - + ' ' - + ' '.join(tissue.tissue_rf + ' ' + tissue.fod + run.command('dwi2fod msmt_csd input.mif ' + f'-lmax {",".join(map(str, lmax))} ' + + ' '.join(f'{tissue.tissue_rf} {tissue.fod}' for tissue in tissues)) # Normalisation in brain mask - run.command('maskfilter mask.mif erode - |' - + ' mtnormalise -mask - -balanced' - + ' -check_factors factors.txt ' - + ' '.join(tissue.fod + ' ' + tissue.fod_norm + run.command('maskfilter mask.mif erode - | ' + 'mtnormalise -mask - -balanced ' + '-check_factors factors.txt ' + + ' '.join(f'{tissue.fod} {tissue.fod_norm}' for tissue in tissues)) app.cleanup([tissue.fod for tissue in tissues]) app.cleanup([tissue.fod_norm for tissue in tissues]) @@ -169,10 +162,10 @@ def __init__(self, name): csf_balance_factor = balance_factors[-1] scale_multiplier = (app.ARGS.reference * math.sqrt(4.0*math.pi)) / (csf_rf_bzero_lzero / csf_balance_factor) - run.command('mrcalc input.mif ' + str(scale_multiplier) + ' -mult - | ' - + 'mrconvert - ' + path.from_user(app.ARGS.output), - mrconvert_keyval=path.from_user(app.ARGS.input, False), + run.command(f'mrcalc input.mif {scale_multiplier} -mult - | ' + f'mrconvert - {app.ARGS.output}', + mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE) if app.ARGS.scale: - matrix.save_vector(path.from_user(app.ARGS.scale, False), [scale_multiplier]) + matrix.save_vector(app.ARGS.scale, [scale_multiplier]) diff --git a/lib/mrtrix3/fsl.py b/lib/mrtrix3/fsl.py index 1fc1c71833..40c19d73f4 100644 --- a/lib/mrtrix3/fsl.py +++ b/lib/mrtrix3/fsl.py @@ -34,12 +34,16 @@ def check_first(prefix, structures): #pylint: disable=unused-variable existing_file_count = sum(os.path.exists(filename) for filename in vtk_files) if existing_file_count != len(vtk_files): if 'SGE_ROOT' in os.environ and os.environ['SGE_ROOT']: - app.console('FSL FIRST job may have been run via SGE; awaiting completion') - app.console('(note however that FIRST may fail silently, and hence this script may hang indefinitely)') + app.console('FSL FIRST job may have been run via SGE; ' + 'awaiting completion') + app.console('(note however that FIRST may fail silently, ' + 'and hence this script may hang indefinitely)') path.wait_for(vtk_files) else: app.DO_CLEANUP = False - raise MRtrixError('FSL FIRST has failed; ' + ('only ' if existing_file_count else '') + str(existing_file_count) + ' of ' + str(len(vtk_files)) + ' structures were segmented successfully (check ' + path.to_scratch('first.logs', False) + ')') + raise MRtrixError('FSL FIRST has failed; ' + f'{"only " if existing_file_count else ""}{existing_file_count} of {len(vtk_files)} structures were segmented successfully ' + f'(check {app.ScratchPath("first.logs")})') @@ -51,7 +55,7 @@ def eddy_binary(cuda): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=import-outside-toplevel if cuda: if shutil.which('eddy_cuda'): - app.debug('Selected soft-linked CUDA version (\'eddy_cuda\')') + app.debug('Selected soft-linked CUDA version ("eddy_cuda")') return 'eddy_cuda' # Cuda versions are now provided with a CUDA trailing version number # Users may not necessarily create a softlink to one of these and @@ -75,7 +79,7 @@ def eddy_binary(cuda): #pylint: disable=unused-variable except ValueError: pass if exe_path: - app.debug('CUDA version ' + str(max_version) + ': ' + exe_path) + app.debug(f'CUDA version {max_version}: {exe_path}') return exe_path app.debug('No CUDA version of eddy found') return '' @@ -96,11 +100,13 @@ def exe_name(name): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=import-outside-toplevel if shutil.which(name): output = name - elif shutil.which('fsl5.0-' + name): - output = 'fsl5.0-' + name - app.warn('Using FSL binary \"' + output + '\" rather than \"' + name + '\"; suggest checking FSL installation') + elif shutil.which(f'fsl5.0-{name}'): + output = f'fsl5.0-{name}' + app.warn(f'Using FSL binary "{output}" rather than "{name}"; ' + 'suggest checking FSL installation') else: - raise MRtrixError('Could not find FSL program \"' + name + '\"; please verify FSL install') + raise MRtrixError(f'Could not find FSL program "{name}"; ' + 'please verify FSL install') app.debug(output) return output @@ -114,13 +120,14 @@ def find_image(name): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=import-outside-toplevel prefix = os.path.join(os.path.dirname(name), os.path.basename(name).split('.')[0]) if os.path.isfile(prefix + suffix()): - app.debug('Image at expected location: \"' + prefix + suffix() + '\"') - return prefix + suffix() + app.debug(f'Image at expected location: "{prefix}{suffix()}"') + return f'{prefix}{suffix()}' for suf in ['.nii', '.nii.gz', '.img']: - if os.path.isfile(prefix + suf): - app.debug('Expected image at \"' + prefix + suffix() + '\", but found at \"' + prefix + suf + '\"') - return prefix + suf - raise MRtrixError('Unable to find FSL output file for path \"' + name + '\"') + if os.path.isfile(f'{prefix}{suf}'): + app.debug(f'Expected image at "{prefix}{suffix()}", ' + f'but found at "{prefix}{suf}"') + return f'{prefix}{suf}' + raise MRtrixError(f'Unable to find FSL output file for path "{name}"') @@ -144,11 +151,16 @@ def suffix(): #pylint: disable=unused-variable app.debug('NIFTI_PAIR -> .img') _SUFFIX = '.img' elif fsl_output_type == 'NIFTI_PAIR_GZ': - raise MRtrixError('MRtrix3 does not support compressed NIFTI pairs; please change FSLOUTPUTTYPE environment variable') + raise MRtrixError('MRtrix3 does not support compressed NIFTI pairs; ' + 'please change FSLOUTPUTTYPE environment variable') elif fsl_output_type: - app.warn('Unrecognised value for environment variable FSLOUTPUTTYPE (\"' + fsl_output_type + '\"): Expecting compressed NIfTIs, but FSL commands may fail') + app.warn('Unrecognised value for environment variable FSLOUTPUTTYPE ' + f'("{fsl_output_type}"): ' + 'Expecting compressed NIfTIs, but FSL commands may fail') _SUFFIX = '.nii.gz' else: - app.warn('Environment variable FSLOUTPUTTYPE not set; FSL commands may fail, or script may fail to locate FSL command outputs') + app.warn('Environment variable FSLOUTPUTTYPE not set; ' + 'FSL commands may fail, ' + 'or script may fail to locate FSL command outputs') _SUFFIX = '.nii.gz' return _SUFFIX diff --git a/lib/mrtrix3/image.py b/lib/mrtrix3/image.py index 37264e7e1f..06431ebff3 100644 --- a/lib/mrtrix3/image.py +++ b/lib/mrtrix3/image.py @@ -19,7 +19,7 @@ # in Python. -import json, math, os, subprocess +import json, math, os, pathlib, subprocess from collections import namedtuple from mrtrix3 import MRtrixError @@ -29,14 +29,15 @@ class Header: def __init__(self, image_path): from mrtrix3 import app, run, utils #pylint: disable=import-outside-toplevel + image_path_str = str(image_path) if isinstance(image_path, pathlib.Path) else image_path filename = utils.name_temporary('json') command = [ run.exe_name(run.version_match('mrinfo')), image_path, '-json_all', filename, '-nodelete' ] if app.VERBOSITY > 1: - app.console('Loading header for image file \'' + image_path + '\'') + app.console(f'Loading header for image file "{image_path_str}"') app.debug(str(command)) result = subprocess.call(command, stdout=None, stderr=None) if result: - raise MRtrixError('Could not access header information for image \'' + image_path + '\'') + raise MRtrixError(f'Could not access header information for image "{image_path_str}"') try: with open(filename, 'r', encoding='utf-8') as json_file: data = json.load(json_file) @@ -63,7 +64,7 @@ def __init__(self, image_path): else: self._keyval = data['keyval'] except Exception as exception: - raise MRtrixError('Error in reading header information from file \'' + image_path + '\'') from exception + raise MRtrixError(f'Error in reading header information from file "{image_path}"') from exception app.debug(str(vars(self))) def name(self): @@ -116,8 +117,8 @@ def axis2dir(string): #pylint: disable=unused-variable elif string == 'k-': direction = [0,0,-1] else: - raise MRtrixError('Unrecognized NIfTI axis & direction specifier: ' + string) - app.debug(string + ' -> ' + str(direction)) + raise MRtrixError(f'Unrecognized NIfTI axis & direction specifier: {string}') + app.debug(f'{string} -> {direction}') return direction @@ -128,14 +129,17 @@ def axis2dir(string): #pylint: disable=unused-variable def check_3d_nonunity(image_in): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=import-outside-toplevel if not isinstance(image_in, Header): - if not isinstance(image_in, str): - raise MRtrixError('Error trying to test \'' + str(image_in) + '\': Not an image header or file path') + if not isinstance(image_in, str) and not isinstance(image_in, app._FilesystemPath): + raise MRtrixError(f'Error trying to test "{image_in}": ' + 'Not an image header or file path') image_in = Header(image_in) if len(image_in.size()) < 3: - raise MRtrixError('Image \'' + image_in.name() + '\' does not contain 3 spatial dimensions') + raise MRtrixError(f'Image "{image_in.name()}" does not contain 3 spatial dimensions') if min(image_in.size()[:3]) == 1: - raise MRtrixError('Image \'' + image_in.name() + '\' does not contain 3D spatial information (has axis with size 1)') - app.debug('Image \'' + image_in.name() + '\' is >= 3D, and does not contain a unity spatial dimension') + raise MRtrixError(f'Image "{image_in.name()}" does not contain 3D spatial information ' + '(has axis with size 1)') + app.debug(f'Image "{image_in.name()}" is >= 3D, ' + 'and does not contain a unity spatial dimension') @@ -148,12 +152,13 @@ def mrinfo(image_path, field): #pylint: disable=unused-variable from mrtrix3 import app, run #pylint: disable=import-outside-toplevel command = [ run.exe_name(run.version_match('mrinfo')), image_path, '-' + field, '-nodelete' ] if app.VERBOSITY > 1: - app.console('Command: \'' + ' '.join(command) + '\' (piping data to local storage)') + app.console(f'Command: "{" ".join(command)}" ' + '(piping data to local storage)') with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=None) as proc: result, dummy_err = proc.communicate() result = result.rstrip().decode('utf-8') if app.VERBOSITY > 1: - app.console('Result: ' + result) + app.console(f'Result: {result}') # Don't exit on error; let the calling function determine whether or not # the absence of the key is an issue return result @@ -167,48 +172,56 @@ def match(image_one, image_two, **kwargs): #pylint: disable=unused-variable, too up_to_dim = kwargs.pop('up_to_dim', 0) check_transform = kwargs.pop('check_transform', True) if kwargs: - raise TypeError('Unsupported keyword arguments passed to image.match(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to image.match(): ' + + str(kwargs)) if not isinstance(image_one, Header): if not isinstance(image_one, str): - raise MRtrixError('Error trying to test \'' + str(image_one) + '\': Not an image header or file path') + raise MRtrixError(f'Error trying to test "{image_one}": ' + 'Not an image header or file path') image_one = Header(image_one) if not isinstance(image_two, Header): if not isinstance(image_two, str): - raise MRtrixError('Error trying to test \'' + str(image_two) + '\': Not an image header or file path') + raise MRtrixError(f'Error trying to test "{image_two}": ' + 'Not an image header or file path') image_two = Header(image_two) - debug_prefix = '\'' + image_one.name() + '\' \'' + image_two.name() + '\'' + debug_prefix = f'"{image_one.name()}" "{image_two.name()}"' # Handle possibility of only checking up to a certain axis if up_to_dim: if up_to_dim > min(len(image_one.size()), len(image_two.size())): - app.debug(debug_prefix + ' dimensionality less than specified maximum (' + str(up_to_dim) + ')') + app.debug(f'{debug_prefix} dimensionality less than specified maximum ({up_to_dim})') return False else: if len(image_one.size()) != len(image_two.size()): - app.debug(debug_prefix + ' dimensionality mismatch (' + str(len(image_one.size())) + ' vs. ' + str(len(image_two.size())) + ')') + app.debug(f'{debug_prefix} dimensionality mismatch ' + f'({len(image_one.size())}) vs. {len(image_two.size())})') return False up_to_dim = len(image_one.size()) # Image dimensions if not image_one.size()[:up_to_dim] == image_two.size()[:up_to_dim]: - app.debug(debug_prefix + ' axis size mismatch (' + str(image_one.size()) + ' ' + str(image_two.size()) + ')') + app.debug(f'{debug_prefix} axis size mismatch ' + f'({image_one.size()} {image_two.size()})') return False # Voxel size for one, two in zip(image_one.spacing()[:up_to_dim], image_two.spacing()[:up_to_dim]): if one and two and not math.isnan(one) and not math.isnan(two): if (abs(two-one) / (0.5*(one+two))) > 1e-04: - app.debug(debug_prefix + ' voxel size mismatch (' + str(image_one.spacing()) + ' ' + str(image_two.spacing()) + ')') + app.debug(f'{debug_prefix} voxel size mismatch ' + f'({image_one.spacing()} {image_two.spacing()})') return False # Image transform if check_transform: for line_one, line_two in zip(image_one.transform(), image_two.transform()): for one, two in zip(line_one[:3], line_two[:3]): if abs(one-two) > 1e-4: - app.debug(debug_prefix + ' transform (rotation) mismatch (' + str(image_one.transform()) + ' ' + str(image_two.transform()) + ')') + app.debug(f'{debug_prefix} transform (rotation) mismatch ' + f'({image_one.transform()} {image_two.transform()})') return False if abs(line_one[3]-line_two[3]) > 1e-2: - app.debug(debug_prefix + ' transform (translation) mismatch (' + str(image_one.transform()) + ' ' + str(image_two.transform()) + ')') + app.debug(f'{debug_prefix} transform (translation) mismatch ' + f'({image_one.transform()} {image_two.transform()})') return False # Everything matches! - app.debug(debug_prefix + ' image match') + app.debug(f'{debug_prefix} image match') return True @@ -225,7 +238,8 @@ def statistics(image_path, **kwargs): #pylint: disable=unused-variable allvolumes = kwargs.pop('allvolumes', False) ignorezero = kwargs.pop('ignorezero', False) if kwargs: - raise TypeError('Unsupported keyword arguments passed to image.statistics(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to image.statistics(): ' + + str(kwargs)) command = [ run.exe_name(run.version_match('mrstats')), image_path ] for stat in IMAGE_STATISTICS: @@ -237,7 +251,8 @@ def statistics(image_path, **kwargs): #pylint: disable=unused-variable if ignorezero: command.append('-ignorezero') if app.VERBOSITY > 1: - app.console('Command: \'' + ' '.join(command) + '\' (piping data to local storage)') + app.console(f'Command: "{" ".join(command)}" ' + '(piping data to local storage)') try: from subprocess import DEVNULL #pylint: disable=import-outside-toplevel @@ -250,7 +265,7 @@ def statistics(image_path, **kwargs): #pylint: disable=unused-variable except AttributeError: pass if proc.returncode: - raise MRtrixError('Error trying to calculate statistics from image \'' + image_path + '\'') + raise MRtrixError(f'Error trying to calculate statistics from image "{image_path}"') stdout_lines = [ line.strip() for line in stdout.decode('cp437').splitlines() ] result = [ ] @@ -261,5 +276,5 @@ def statistics(image_path, **kwargs): #pylint: disable=unused-variable if len(result) == 1: result = result[0] if app.VERBOSITY > 1: - app.console('Result: ' + str(result)) + app.console(f'Result: {result}') return result diff --git a/lib/mrtrix3/matrix.py b/lib/mrtrix3/matrix.py index ad0f34f5fd..6a536ded6d 100644 --- a/lib/mrtrix3/matrix.py +++ b/lib/mrtrix3/matrix.py @@ -29,20 +29,21 @@ def dot(input_a, input_b): #pylint: disable=unused-variable if not input_a: if input_b: - raise MRtrixError('Dimension mismatch (0 vs. ' + str(len(input_b)) + ')') + raise MRtrixError('Dimension mismatch ' + f'(0 vs. {len(input_b)})') return [ ] if is_2d_matrix(input_a): if not is_2d_matrix(input_b): raise MRtrixError('Both inputs must be either 1D vectors or 2D matrices') if len(input_a[0]) != len(input_b): - raise MRtrixError('Invalid dimensions for matrix dot product(' + \ - str(len(input_a)) + 'x' + str(len(input_a[0])) + ' vs. ' + \ - str(len(input_b)) + 'x' + str(len(input_b[0])) + ')') + raise MRtrixError('Invalid dimensions for matrix dot product ' + f'({len(input_a)}x{len(input_a[0])} vs. {len(input_b)}x{len(input_b[0])})') return [[sum(x*y for x,y in zip(a_row,b_col)) for b_col in zip(*input_b)] for a_row in input_a] if is_2d_matrix(input_b): raise MRtrixError('Both inputs must be either 1D vectors or 2D matrices') if len(input_a) != len(input_b): - raise MRtrixError('Dimension mismatch (' + str(len(input_a)) + ' vs. ' + str(len(input_b)) + ')') + raise MRtrixError('Dimension mismatch ' + f'({len(input_a)} vs. {len(input_b)})') return sum(x*y for x,y in zip(input_a, input_b)) @@ -88,7 +89,8 @@ def load_numeric(filename, **kwargs): encoding = kwargs.pop('encoding', 'latin1') errors = kwargs.pop('errors', 'ignore') if kwargs: - raise TypeError('Unsupported keyword arguments passed to matrix.load_numeric(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to matrix.load_numeric(): ' + + str(kwargs)) def decode(line): if isinstance(line, bytes): @@ -124,7 +126,7 @@ def load_matrix(filename, **kwargs): #pylint: disable=unused-variable columns = len(data[0]) for line in data[1:]: if len(line) != columns: - raise MRtrixError('Inconsistent number of columns in matrix text file "' + filename + '"') + raise MRtrixError(f'Inconsistent number of columns in matrix text file "{filename}"') return data @@ -134,13 +136,16 @@ def load_transform(filename, **kwargs): #pylint: disable=unused-variable data = load_matrix(filename, **kwargs) if len(data) == 4: if any(a!=b for a, b in zip(data[3], _TRANSFORM_LAST_ROW)): - raise MRtrixError('File "' + filename + '" does not contain a valid transform (fourth line contains values other than "0,0,0,1")') + raise MRtrixError(f'File "{filename}" does not contain a valid transform ' + '(fourth line contains values other than "0,0,0,1")') elif len(data) == 3: data.append(_TRANSFORM_LAST_ROW) else: - raise MRtrixError('File "' + filename + '" does not contain a valid transform (must contain 3 or 4 lines)') + raise MRtrixError(f'File "{filename}" does not contain a valid transform ' + '(must contain 3 or 4 lines)') if len(data[0]) != 4: - raise MRtrixError('File "' + filename + '" does not contain a valid transform (must contain 4 columns)') + raise MRtrixError(f'File "{filename}" does not contain a valid transform ' + '(must contain 4 columns)') return data @@ -152,7 +157,8 @@ def load_vector(filename, **kwargs): #pylint: disable=unused-variable return data[0] for line in data: if len(line) != 1: - raise MRtrixError('File "' + filename + '" does not contain vector data (multiple columns detected)') + raise MRtrixError(f'File "{filename}" does not contain vector data ' + '(multiple columns detected)') return [ line[0] for line in data ] @@ -169,10 +175,12 @@ def save_numeric(filename, data, **kwargs): encoding = kwargs.pop('encoding', None) force = kwargs.pop('force', False) if kwargs: - raise TypeError('Unsupported keyword arguments passed to matrix.save_numeric(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to matrix.save_numeric(): ' + + str(kwargs)) if not force and os.path.exists(filename): - raise MRtrixError('output file "' + filename + '" already exists (use -force option to force overwrite)') + raise MRtrixError(f'output file "{filename}" already exists ' + '(use -force option to force overwrite)') encode_args = {'errors': 'ignore'} if encoding: @@ -192,7 +200,7 @@ def save_numeric(filename, data, **kwargs): if add_to_command_history and COMMAND_HISTORY_STRING: if 'command_history' in header: - header['command_history'] += '\n' + COMMAND_HISTORY_STRING + header['command_history'] += f'\n{COMMAND_HISTORY_STRING}' else: header['command_history'] = COMMAND_HISTORY_STRING @@ -217,7 +225,7 @@ def save_numeric(filename, data, **kwargs): with io.open(file_descriptor, 'wb') as outfile: for key, value in sorted(header.items()): for line in value.splitlines(): - outfile.write((comments + key + ': ' + line + newline).encode(**encode_args)) + outfile.write(f'{comments}{key}: {line}{newline}'.encode(**encode_args)) if data: if isinstance(data[0], list): @@ -233,7 +241,7 @@ def save_numeric(filename, data, **kwargs): for key, value in sorted(footer.items()): for line in value.splitlines(): - outfile.write((comments + key + ': ' + line + newline).encode(**encode_args)) + outfile.write(f'{comments}{key}: {line}{newline}'.encode(**encode_args)) @@ -259,7 +267,8 @@ def save_transform(filename, data, **kwargs): #pylint: disable=unused-variable raise TypeError('Input to matrix.save_transform() must be a 3x4 or 4x4 matrix') if len(data) == 4: if any(a!=b for a, b in zip(data[3], _TRANSFORM_LAST_ROW)): - raise TypeError('Input to matrix.save_transform() is not a valid affine matrix (fourth line contains values other than "0,0,0,1")') + raise TypeError('Input to matrix.save_transform() is not a valid affine matrix ' + '(fourth line contains values other than "0,0,0,1")') save_matrix(filename, data, **kwargs) elif len(data) == 3: padded_data = data[:] diff --git a/lib/mrtrix3/path.py b/lib/mrtrix3/path.py index cc618fefee..ecadec9829 100644 --- a/lib/mrtrix3/path.py +++ b/lib/mrtrix3/path.py @@ -17,7 +17,7 @@ -import ctypes, inspect, os, shlex, shutil, subprocess, time +import ctypes, inspect, os, shutil, subprocess, time @@ -27,11 +27,12 @@ def all_in_dir(directory, **kwargs): #pylint: disable=unused-variable dir_path = kwargs.pop('dir_path', True) ignore_hidden_files = kwargs.pop('ignore_hidden_files', True) if kwargs: - raise TypeError('Unsupported keyword arguments passed to path.all_in_dir(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to path.all_in_dir(): ' + + str(kwargs)) def is_hidden(directory, filename): if utils.is_windows(): try: - attrs = ctypes.windll.kernel32.GetFileAttributesW("%s" % str(os.path.join(directory, filename))) + attrs = ctypes.windll.kernel32.GetFileAttributesW(str(os.path.join(directory, filename))) assert attrs != -1 return bool(attrs & 2) except (AttributeError, AssertionError): @@ -44,25 +45,6 @@ def is_hidden(directory, filename): -# Get the full absolute path to a user-specified location. -# This function serves two purposes: -# - To get the intended user-specified path when a script is operating inside a scratch directory, rather than -# the directory that was current when the user specified the path; -# - To add quotation marks where the output path is being interpreted as part of a full command string -# (e.g. to be passed to run.command()); without these quotation marks, paths that include spaces would be -# erroneously split, subsequently confusing whatever command is being invoked. -# If the filesystem path provided by the script is to be interpreted in isolation, rather than as one part -# of a command string, then parameter 'escape' should be set to False in order to not add quotation marks -def from_user(filename, escape=True): #pylint: disable=unused-variable - from mrtrix3 import app #pylint: disable=import-outside-toplevel - fullpath = os.path.abspath(os.path.join(app.WORKING_DIR, filename)) - if escape: - fullpath = shlex.quote(fullpath) - app.debug(filename + ' -> ' + fullpath) - return fullpath - - - # Determine the name of a sub-directory containing additional data / source files for a script # This can be algorithm files in lib/mrtrix3/, or data files in share/mrtrix3/ # This function appears here rather than in the algorithm module as some scripts may @@ -96,20 +78,6 @@ def shared_data_path(): #pylint: disable=unused-variable -# Get the full absolute path to a location in the script's scratch directory -# Also deals with the potential for special characters in a path (e.g. spaces) by wrapping in quotes, -# as long as parameter 'escape' is true (if the path yielded by this function is to be interpreted in -# isolation rather than as one part of a command string, parameter 'escape' should be set to False) -def to_scratch(filename, escape=True): #pylint: disable=unused-variable - from mrtrix3 import app #pylint: disable=import-outside-toplevel - fullpath = os.path.abspath(os.path.join(app.SCRATCH_DIR, filename)) - if escape: - fullpath = shlex.quote(fullpath) - app.debug(filename + ' -> ' + fullpath) - return fullpath - - - # Wait until a particular file not only exists, but also does not have any # other process operating on it (so hopefully whatever created it has # finished its work) @@ -182,7 +150,8 @@ def num_in_use(data): # Wait until all files exist num_exist = num_exit(paths) if num_exist != len(paths): - progress = app.ProgressBar('Waiting for creation of ' + (('new item \"' + paths[0] + '\"') if len(paths) == 1 else (str(len(paths)) + ' new items')), len(paths)) + item_message = f'new item "{paths[0]}"' if len(paths) == 1 else f'{len(paths)} new items' + progress = app.ProgressBar(f'Waiting for creation of {item_message}', len(paths)) for _ in range(num_exist): progress.increment() delay = 1.0/1024.0 @@ -198,7 +167,7 @@ def num_in_use(data): delay = 1.0/1024.0 progress.done() else: - app.debug('Item' + ('s' if len(paths) > 1 else '') + ' existed immediately') + app.debug(f'{"Items" if len(paths) > 1 else "Item"} existed immediately') # Check to see if active use of the file(s) needs to be tested at_least_one_file = False @@ -207,7 +176,8 @@ def num_in_use(data): at_least_one_file = True break if not at_least_one_file: - app.debug('No target files, directories only; not testing for finalization') + app.debug('No target files, directories only; ' + 'not testing for finalization') return # Can we query the in-use status of any of these paths @@ -218,10 +188,11 @@ def num_in_use(data): # Wait until all files are not in use if not num_in_use: - app.debug('Item' + ('s' if len(paths) > 1 else '') + ' immediately ready') + app.debug(f'{"Items" if len(paths) > 1 else "Item"} immediately ready') return - progress = app.ProgressBar('Waiting for finalization of ' + (('new file \"' + paths[0] + '\"') if len(paths) == 1 else (str(len(paths)) + ' new files'))) + item_message = f'new file "{paths[0]}"' if len(paths) == 1 else f'{len(paths)} new files' + progress = app.ProgressBar('Waiting for finalization of {item_message}') for _ in range(len(paths) - num_in_use): progress.increment() delay = 1.0/1024.0 diff --git a/lib/mrtrix3/phaseencoding.py b/lib/mrtrix3/phaseencoding.py index 91a70d1e7f..8eb3cf57bd 100644 --- a/lib/mrtrix3/phaseencoding.py +++ b/lib/mrtrix3/phaseencoding.py @@ -27,7 +27,9 @@ def direction(string): #pylint: disable=unused-variable try: pe_axis = abs(int(string)) if pe_axis > 2: - raise MRtrixError('When specified as a number, phase encoding axis must be either 0, 1 or 2 (positive or negative)') + raise MRtrixError('When specified as a number, ' + 'phase encoding axis must be either 0, 1 or 2 ' + '(positive or negative)') reverse = (string.contains('-')) # Allow -0 pe_dir = [0,0,0] if reverse: @@ -61,8 +63,8 @@ def direction(string): #pylint: disable=unused-variable elif string == 'k-': pe_dir = [0,0,-1] else: - raise MRtrixError('Unrecognized phase encode direction specifier: ' + string) # pylint: disable=raise-missing-from - app.debug(string + ' -> ' + str(pe_dir)) + raise MRtrixError(f'Unrecognized phase encode direction specifier: {string}') # pylint: disable=raise-missing-from + app.debug(f'{string} -> {pe_dir}') return pe_dir @@ -73,7 +75,8 @@ def get_scheme(arg): #pylint: disable=unused-variable from mrtrix3 import app, image #pylint: disable=import-outside-toplevel if not isinstance(arg, image.Header): if not isinstance(arg, str): - raise TypeError('Error trying to derive phase-encoding scheme from \'' + str(arg) + '\': Not an image header or file path') + raise TypeError(f'Error trying to derive phase-encoding scheme from "{arg}": ' + 'Not an image header or file path') arg = image.Header(arg) if 'pe_scheme' in arg.keyval(): app.debug(str(arg.keyval()['pe_scheme'])) @@ -96,7 +99,8 @@ def save(filename, scheme, **kwargs): #pylint: disable=unused-variable add_to_command_history = bool(kwargs.pop('add_to_command_history', True)) header = kwargs.pop('header', { }) if kwargs: - raise TypeError('Unsupported keyword arguments passed to phaseencoding.save(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to phaseencoding.save(): ' + + str(kwargs)) if not scheme: raise MRtrixError('phaseencoding.save() cannot be run on an empty scheme') @@ -104,7 +108,7 @@ def save(filename, scheme, **kwargs): #pylint: disable=unused-variable raise TypeError('Input to phaseencoding.save() must be a 2D matrix') if len(scheme[0]) != 4: raise MRtrixError('Input to phaseencoding.save() not a valid scheme ' - '(contains ' + str(len(scheme[0])) + ' columns rather than 4)') + f'(contains {len(scheme[0])} columns rather than 4)') if header: if isinstance(header, str): @@ -120,7 +124,7 @@ def save(filename, scheme, **kwargs): #pylint: disable=unused-variable if add_to_command_history and COMMAND_HISTORY_STRING: if 'command_history' in header: - header['command_history'] += '\n' + COMMAND_HISTORY_STRING + header['command_history'] += f'\n{COMMAND_HISTORY_STRING}' else: header['command_history'] = COMMAND_HISTORY_STRING @@ -129,4 +133,4 @@ def save(filename, scheme, **kwargs): #pylint: disable=unused-variable for line in value.splitlines(): outfile.write('# ' + key + ': ' + line + '\n') for line in scheme: - outfile.write('{:.0f} {:.0f} {:.0f} {:.15g}\n'.format(*line)) + outfile.write(f'{line[0]:.0f} {line[1]:.0f} {line[2]:.0f} {line[3]:.15g}\n') diff --git a/lib/mrtrix3/run.py b/lib/mrtrix3/run.py index 75a05c2fab..3ae730a7e8 100644 --- a/lib/mrtrix3/run.py +++ b/lib/mrtrix3/run.py @@ -13,7 +13,7 @@ # # For more details, see http://www.mrtrix.org/. -import collections, itertools, os, shlex, shutil, signal, string, subprocess, sys, tempfile, threading +import collections, itertools, os, pathlib, shlex, shutil, signal, string, subprocess, sys, tempfile, threading from mrtrix3 import ANSI, BIN_PATH, COMMAND_HISTORY_STRING, EXE_LIST, MRtrixBaseError, MRtrixError IOStream = collections.namedtuple('IOStream', 'handle filename') @@ -242,7 +242,8 @@ def quote_nonpipe(item): # Reference rather than copying env = shared.env if kwargs: - raise TypeError('Unsupported keyword arguments passed to run.command(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to run.command(): ' + + str(kwargs)) if shell and mrconvert_keyval: raise TypeError('Cannot use "mrconvert_keyval=" parameter in shell mode') @@ -256,12 +257,13 @@ def quote_nonpipe(item): if isinstance(cmd, list): if shell: - raise TypeError('When using run.command() with shell=True, input must be a text string') + raise TypeError('When using run.command() with shell=True, ' + 'input must be a text string') cmdstring = '' cmdsplit = [] for entry in cmd: if isinstance(entry, str): - cmdstring += (' ' if cmdstring else '') + quote_nonpipe(entry) + cmdstring += f'{" " if cmdstring else ""}{quote_nonpipe(entry)}' cmdsplit.append(entry) elif isinstance(entry, list): assert all(isinstance(item, str) for item in entry) @@ -269,14 +271,19 @@ def quote_nonpipe(item): common_prefix = os.path.commonprefix(entry) common_suffix = os.path.commonprefix([i[::-1] for i in entry])[::-1] if common_prefix == entry[0] and common_prefix == common_suffix: - cmdstring += (' ' if cmdstring else '') + '[' + entry[0] + ' (x' + str(len(entry)) + ')]' + cmdstring += f'{" " if cmdstring else ""}[{entry[0]} (x{len(entry)})]' else: - cmdstring += (' ' if cmdstring else '') + '[' + common_prefix + '*' + common_suffix + ' (' + str(len(entry)) + ' items)]' + cmdstring += f'{" " if cmdstring else ""}[{common_prefix}*{common_suffix} ({len(entry)} items)]' else: - cmdstring += (' ' if cmdstring else '') + quote_nonpipe(entry[0]) + cmdstring += f'{" " if cmdstring else ""}{quote_nonpipe(entry[0])}' cmdsplit.extend(entry) + elif isinstance(entry, pathlib.Path): + cmdstring += f'{" " if cmdstring else ""}{shlex.quote(str(entry))}' + cmdsplit.append(str(entry)) else: - raise TypeError('When run.command() is provided with a list as input, entries in the list must be either strings or lists of strings') + raise TypeError('When run.command() is provided with a list as input, ' + 'entries in the list must be either strings or lists of strings; ' + f'received: {repr(cmd)}') elif isinstance(cmd, str): cmdstring = cmd # Split the command string by spaces, preserving anything encased within quotation marks @@ -285,17 +292,17 @@ def quote_nonpipe(item): else: # Native Windows Python cmdsplit = [ entry.strip('\"') for entry in shlex.split(cmd, posix=False) ] else: - raise TypeError('run.command() function only operates on strings, or lists of strings') + raise TypeError('run.command() function only operates on strings or lists of strings') if shared.get_continue(): if shared.trigger_continue(cmdsplit): - app.debug('Detected last file in command \'' + cmdstring + '\'; this is the last run.command() / run.function() call that will be skipped') + app.debug(f'Detected last file in command "{cmdstring}"; ' + 'this is the last run.command() / run.function() call that will be skipped') if shared.verbosity: - sys.stderr.write(ANSI.execute + 'Skipping command:' + ANSI.clear + ' ' + cmdstring + '\n') + sys.stderr.write(f'{ANSI.execute}Skipping command:{ANSI.clear} {cmdstring}\n') sys.stderr.flush() return CommandReturn('', '') - # If operating in shell=True mode, handling of command execution is significantly different: # Unmodified command string is executed using subprocess, with the shell being responsible for its parsing # Only a single process per run.command() invocation is possible (since e.g. any piping will be @@ -306,9 +313,9 @@ def quote_nonpipe(item): cmdstack = [ cmdsplit ] with shared.lock: - app.debug('To execute: ' + str(cmdsplit)) + app.debug(f'To execute: {cmdsplit}') if (shared.verbosity and show) or shared.verbosity > 1: - sys.stderr.write(ANSI.execute + 'Command:' + ANSI.clear + ' ' + cmdstring + '\n') + sys.stderr.write(f'{ANSI.execute}Command:{ANSI.clear} {cmdstring}\n') sys.stderr.flush() # No locking required for actual creation of new process this_stdout = shared.make_temporary_file() @@ -326,7 +333,7 @@ def quote_nonpipe(item): pre_result = command(cmdsplit[:index]) if operator == '||': with shared.lock: - app.debug('Due to success of "' + cmdsplit[:index] + '", "' + cmdsplit[index+1:] + '" will not be run') + app.debug(f'Due to success of "{cmdsplit[:index]}", "{cmdsplit[index+1:]}" will not be run') return pre_result except MRtrixCmdError: if operator == '&&': @@ -341,9 +348,13 @@ def quote_nonpipe(item): if mrconvert_keyval: if cmdstack[-1][0] != 'mrconvert': - raise TypeError('Argument "mrconvert_keyval=" can only be used if the mrconvert command is being invoked') - assert not (mrconvert_keyval[0] in [ '\'', '"' ] or mrconvert_keyval[-1] in [ '\'', '"' ]) - cmdstack[-1].extend([ '-copy_properties', mrconvert_keyval ]) + raise TypeError('Argument "mrconvert_keyval=" can only be used ' + 'if the mrconvert command is being invoked') + if isinstance(mrconvert_keyval, app._FilesystemPath): + cmdstack[-1].extend([ '-copy_properties', str(mrconvert_keyval) ]) + else: + assert not (mrconvert_keyval[0] in [ '\'', '"' ] or mrconvert_keyval[-1] in [ '\'', '"' ]) + cmdstack[-1].extend([ '-copy_properties', mrconvert_keyval ]) if COMMAND_HISTORY_STRING: cmdstack[-1].extend([ '-append_property', 'command_history', COMMAND_HISTORY_STRING ]) @@ -370,7 +381,7 @@ def quote_nonpipe(item): with shared.lock: app.debug('To execute: ' + str(cmdstack)) if (shared.verbosity and show) or shared.verbosity > 1: - sys.stderr.write(ANSI.execute + 'Command:' + ANSI.clear + ' ' + cmdstring + '\n') + sys.stderr.write(f'{ANSI.execute}Command:{ANSI.clear} {cmdstring}\n') sys.stderr.flush() this_command_index = shared.get_command_index() @@ -415,7 +426,8 @@ def quote_nonpipe(item): stderrdata = b'' do_indent = True while True: - # Have to read one character at a time: Waiting for a newline character using e.g. readline() will prevent MRtrix progressbars from appearing + # Have to read one character at a time: + # Waiting for a newline character using e.g. readline() will prevent MRtrix progressbars from appearing byte = process.stderr.read(1) stderrdata += byte char = byte.decode('cp1252', errors='ignore') @@ -492,21 +504,27 @@ def function(fn_to_execute, *args, **kwargs): #pylint: disable=unused-variable show = kwargs.pop('show', True) - fnstring = fn_to_execute.__module__ + '.' + fn_to_execute.__name__ + \ - '(' + ', '.join(['\'' + str(a) + '\'' if isinstance(a, str) else str(a) for a in args]) + \ - (', ' if (args and kwargs) else '') + \ - ', '.join([key+'='+str(value) for key, value in kwargs.items()]) + ')' + def quoted(text): + return f'"{text}"' + def format_keyval(key, value): + return f'{key}={value}' + + fnstring = f'{fn_to_execute.__module__}.{fn_to_execute.__name__}' \ + f'({", ".join([quoted(a) if isinstance(a, str) else str(a) for a in args])}' \ + f'{", " if (args and kwargs) else ""}' \ + f'{", ".join([format_keyval(key, value) for key, value in kwargs.items()])}' if shared.get_continue(): if shared.trigger_continue(args) or shared.trigger_continue(kwargs.values()): - app.debug('Detected last file in function \'' + fnstring + '\'; this is the last run.command() / run.function() call that will be skipped') + app.debug(f'Detected last file in function "{fnstring}"; ' + 'this is the last run.command() / run.function() call that will be skipped') if shared.verbosity: - sys.stderr.write(ANSI.execute + 'Skipping function:' + ANSI.clear + ' ' + fnstring + '\n') + sys.stderr.write(f'{ANSI.execute}Skipping function:{ANSI.clear} {fnstring}\n') sys.stderr.flush() return None if (shared.verbosity and show) or shared.verbosity > 1: - sys.stderr.write(ANSI.execute + 'Function:' + ANSI.clear + ' ' + fnstring + '\n') + sys.stderr.write(f'{ANSI.execute}Function:{ANSI.clear} {fnstring}\n') sys.stderr.flush() # Now we need to actually execute the requested function @@ -538,16 +556,16 @@ def exe_name(item): path = item elif os.path.isfile(os.path.join(BIN_PATH, item)): path = item - elif os.path.isfile(os.path.join(BIN_PATH, item + '.exe')): + elif os.path.isfile(os.path.join(BIN_PATH, f'{item}.exe')): path = item + '.exe' elif shutil.which(item) is not None: path = item - elif shutil.which(item + '.exe') is not None: - path = item + '.exe' + elif shutil.which(f'{item}.exe') is not None: + path = f'{item}.exe' # If it can't be found, return the item as-is else: path = item - app.debug(item + ' -> ' + path) + app.debug(f'{item} -> {path}') return path @@ -560,17 +578,17 @@ def exe_name(item): def version_match(item): from mrtrix3 import app #pylint: disable=import-outside-toplevel if not item in EXE_LIST: - app.debug('Command ' + item + ' not found in MRtrix3 bin/ directory') + app.debug(f'Command "{item}" not found in MRtrix3 bin/ directory') return item exe_path_manual = os.path.join(BIN_PATH, exe_name(item)) if os.path.isfile(exe_path_manual): - app.debug('Version-matched executable for ' + item + ': ' + exe_path_manual) + app.debug(f'Version-matched executable for "{item}": {exe_path_manual}') return exe_path_manual exe_path_sys = shutil.which(exe_name(item)) if exe_path_sys and os.path.isfile(exe_path_sys): - app.debug('Using non-version-matched executable for ' + item + ': ' + exe_path_sys) + app.debug(f'Using non-version-matched executable for "{item}": {exe_path_sys}') return exe_path_sys - raise MRtrixError('Unable to find executable for MRtrix3 command ' + item) + raise MRtrixError(f'Unable to find executable for MRtrix3 command "{item}"') @@ -586,7 +604,7 @@ def _shebang(item): if path == item: path = shutil.which(exe_name(item)) if not path: - app.debug('File \"' + item + '\": Could not find file to query') + app.debug(f'File "{item}": Could not find file to query') return [] # Read the first 1024 bytes of the file with open(path, 'rb') as file_in: @@ -597,7 +615,7 @@ def _shebang(item): try: line = str(line.decode('utf-8')) except UnicodeDecodeError: - app.debug('File \"' + item + '\": Not a text file') + app.debug(f'File "{item}": Not a text file') return [] line = line.strip() if len(line) > 2 and line[0:2] == '#!': @@ -609,19 +627,21 @@ def _shebang(item): # as long as Python2 is not explicitly requested if os.path.basename(shebang[0]) == 'env': if len(shebang) < 2: - app.warn('Invalid shebang in script file \"' + item + '\" (missing interpreter after \"env\")') + app.warn(f'Invalid shebang in script file "{item}" ' + '(missing interpreter after "env")') return [] if shebang[1] == 'python' or shebang[1] == 'python3': if not sys.executable: - app.warn('Unable to self-identify Python interpreter; file \"' + item + '\" not guaranteed to execute on same version') + app.warn('Unable to self-identify Python interpreter; ' + f'file "{item}" not guaranteed to execute on same version') return [] shebang = [ sys.executable ] + shebang[2:] - app.debug('File \"' + item + '\": Using current Python interpreter') + app.debug(f'File "{item}": Using current Python interpreter') elif utils.is_windows(): shebang = [ os.path.abspath(shutil.which(exe_name(shebang[1]))) ] + shebang[2:] elif utils.is_windows(): shebang = [ os.path.abspath(shutil.which(exe_name(os.path.basename(shebang[0])))) ] + shebang[1:] - app.debug('File \"' + item + '\": string \"' + line + '\": ' + str(shebang)) + app.debug(f'File "{item}": string "{line}": {shebang}') return shebang - app.debug('File \"' + item + '\": No shebang found') + app.debug(f'File "{item}": No shebang found') return [] diff --git a/lib/mrtrix3/utils.py b/lib/mrtrix3/utils.py index 6e76926a86..50046808f0 100644 --- a/lib/mrtrix3/utils.py +++ b/lib/mrtrix3/utils.py @@ -47,9 +47,9 @@ def __init__(self, message, value): self.progress.done() self.valid = False else: - raise TypeError('Construction of RunList class expects either an ' - 'integer (number of commands/functions to run), or a ' - 'list of command strings to execute') + raise TypeError('Construction of RunList class expects either ' + 'an integer (number of commands/functions to run), ' + 'or a list of command strings to execute') def command(self, cmd, **kwargs): from mrtrix3 import run #pylint: disable=import-outside-toplevel assert self.valid @@ -83,7 +83,8 @@ def load_keyval(filename, **kwargs): #pylint: disable=unused-variable encoding = kwargs.pop('encoding', 'latin1') errors = kwargs.pop('errors', 'ignore') if kwargs: - raise TypeError('Unsupported keyword arguments passed to utils.load_keyval(): ' + str(kwargs)) + raise TypeError('Unsupported keyword arguments passed to utils.load_keyval(): ' + + str(kwargs)) def decode(line): if isinstance(line, bytes): @@ -121,7 +122,7 @@ def name_temporary(suffix): #pylint: disable=unused-variable suffix = suffix.lstrip('.') while os.path.exists(full_path): random_string = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for x in range(6)) - full_path = os.path.join(dir_path, prefix + random_string + '.' + suffix) + full_path = os.path.join(dir_path, f'{prefix}{random_string}.{suffix}') app.debug(full_path) return full_path @@ -153,8 +154,8 @@ def make_dir(path): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=import-outside-toplevel try: os.makedirs(path) - app.debug('Created directory ' + path) + app.debug(f'Created directory {path}') except OSError as exception: if exception.errno != errno.EEXIST: raise - app.debug('Directory \'' + path + '\' already exists') + app.debug(f'Directory "{path}" already exists') diff --git a/testing/pylint.rc b/testing/pylint.rc index 1c27f7dea6..97a07bb23c 100644 --- a/testing/pylint.rc +++ b/testing/pylint.rc @@ -48,7 +48,7 @@ confidence= # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" #disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call -disable=line-too-long,multiple-imports,missing-docstring,global-statement,too-many-branches,too-many-statements,too-many-nested-blocks,too-many-locals,too-many-instance-attributes,too-few-public-methods,too-many-arguments,too-many-lines,consider-using-f-string +disable=line-too-long,multiple-imports,missing-docstring,global-statement,too-many-branches,too-many-statements,too-many-nested-blocks,too-many-locals,too-many-instance-attributes,too-few-public-methods,too-many-arguments,too-many-lines # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where diff --git a/testing/scripts/tests/dwi2mask b/testing/scripts/tests/dwi2mask index 9f7cdf3e0a..367342ba99 100644 --- a/testing/scripts/tests/dwi2mask +++ b/testing/scripts/tests/dwi2mask @@ -17,7 +17,7 @@ dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_fsl_fnirtcmdlin dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_fsl_fnirtconfig.mif -software fsl -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -config Dwi2maskTemplateFSLFnirtConfig $(pwd)/dwi2mask/fnirt_config.cnf -force dwi2mask consensus tmp-sub-01_dwi.mif ../tmp/dwi2mask/consensus_output.mif -masks ../tmp/dwi2mask/consensus_masks.mif -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -force dwi2mask fslbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/fslbet_default.mif -force -dwi2mask fslbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/fslbet_options.mif -bet_f 0.5 -bet_g 0.0 -bet_c 14 18 16 -bet_r 130.0 -force +dwi2mask fslbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/fslbet_options.mif -bet_f 0.5 -bet_g 0.0 -bet_c 14,18,16 -bet_r 130.0 -force dwi2mask fslbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/fslbet_rescale.mif -rescale -force dwi2mask hdbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/hdbet.mif -force dwi2mask legacy tmp-sub-01_dwi.mif ../tmp/dwi2mask/legacy.mif -force && testing_diff_image ../tmp/dwi2mask/legacy.mif dwi2mask/legacy.mif.gz From b31c9d9631a6dbdadd9ac7a7e3a00e395c18141d Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 14 Feb 2024 11:19:27 +1100 Subject: [PATCH 32/75] New unit test for Python CLI typing --- run_tests | 3 ++- testing/tests/python_cli | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 testing/tests/python_cli diff --git a/run_tests b/run_tests index 3cb38f83c2..757fb19e72 100755 --- a/run_tests +++ b/run_tests @@ -188,7 +188,8 @@ EOD [[ -n "$cmd" ]] || continue echo -n '# command: '$cmd >> $LOGFILE ( - export PATH="$(pwd)/testing/bin:${MRTRIX_BINDIR}:$PATH"; + export PATH="$(pwd)/testing/bin:${MRTRIX_BINDIR}:$PATH" + export PYTHONPATH="$(pwd)/lib" cd $datadir eval $cmd ) > .__tmp.log 2>&1 diff --git a/testing/tests/python_cli b/testing/tests/python_cli new file mode 100644 index 0000000000..eccf20d1d8 --- /dev/null +++ b/testing/tests/python_cli @@ -0,0 +1,35 @@ +mkdir -p tmp-newdirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -untyped my_untyped -string my_string -bool false -int_builtin 0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_builtin 0.0 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -intseq 1,2,3 -floatseq 0.1,0.2,0.3 -dirin tmp-dirin/ -dirout tmp-dirout/ -filein tmp-filein.txt -fileout tmp-fileout.txt -tracksin tmp-tracksin.tck -tracksout tmp-tracksout.tck -various my_various +testing_python_cli -bool false +testing_python_cli -bool False +testing_python_cli -bool FALSE +testing_python_cli -bool true +testing_python_cli -bool True +testing_python_cli -bool TRUE +testing_python_cli -bool 0 +testing_python_cli -bool 1 +testing_python_cli -bool 2 +testing_python_cli -bool NotABool && false || true +testing_python_cli -int_builtin 0.1 && false || true +testing_python_cli -int_builtin NotAnInt && false || true +testing_python_cli -int_unbound 0.1 && false || true +testing_python_cli -int_unbound NotAnInt && false || true +testing_python_cli -int_nonneg -1 && false || true +testing_python_cli -int_bound 101 && false || true +testing_python_cli -float_builtin NotAFloat && false || true +testing_python_cli -float_unbound NotAFloat && false || true +testing_python_cli -float_nonneg -0.1 && false || true +testing_python_cli -float_bound 1.1 && false || true +testing_python_cli -intseq 0.1,0.2,0.3 && false || true +testing_python_cli -intseq Not,An,Int,Seq && false || true +testing_python_cli -floatseq Not,A,Float,Seq && false || true +testing_python_cli -dirin does/not/exist/ && false || true +mkdir -p tmp-dirout/ && testing_python_cli -dirout tmp-dirout/ && false || true +testing_python_cli -dirout tmp-dirout/ -force +testing_python_cli -filein does/not/exist.txt && false || true +touch tmp-fileout.txt && testing_python_cli -fileout tmp-fileout.txt && false || true +touch tmp-fileout.txt && testing_python_cli -fileout tmp-fileout.txt -force +testing_python_cli -tracksin does/not/exist.txt && false || true +testing_python_cli -tracksin tmp-filein.txt && false || true +touch tmp-tracksout.tck && testing_python_cli -tracksout tmp-tracksout.tck && false || true +testing_python_cli -tracksout tmp-tracksout.tck -force +testing_python_cli -tracksout tmp-tracksout.txt && false || true From 7791a38a629040bb39c0795fa569c24828659ca3 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 19 Feb 2024 10:15:47 +1100 Subject: [PATCH 33/75] Fix Python help pages for bound integers & floats --- lib/mrtrix3/app.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 438812560e..11daa9e4c2 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -656,7 +656,7 @@ def Int(min_value=None, max_value=None): # pylint: disable=invalid-name assert min_value is None or isinstance(min_value, int) assert max_value is None or isinstance(max_value, int) assert min_value is None or max_value is None or max_value >= min_value - class IntChecker(Parser.CustomTypeBase): + class IntBounded(Parser.CustomTypeBase): def __call__(self, input_value): try: value = int(input_value) @@ -670,13 +670,13 @@ def __call__(self, input_value): @staticmethod def _typestring(): return f'INT {-sys.maxsize - 1 if min_value is None else min_value} {sys.maxsize if max_value is None else max_value}' - return IntChecker() + return IntBounded() def Float(min_value=None, max_value=None): # pylint: disable=invalid-name assert min_value is None or isinstance(min_value, float) assert max_value is None or isinstance(max_value, float) assert min_value is None or max_value is None or max_value >= min_value - class FloatChecker(Parser.CustomTypeBase): + class FloatBounded(Parser.CustomTypeBase): def __call__(self, input_value): try: value = float(input_value) @@ -690,7 +690,7 @@ def __call__(self, input_value): @staticmethod def _typestring(): return f'FLOAT {"-inf" if min_value is None else str(min_value)} {"inf" if max_value is None else str(max_value)}' - return FloatChecker() + return FloatBounded() class SequenceInt(CustomTypeBase): def __call__(self, input_value): @@ -1118,7 +1118,10 @@ def print_group_options(group): elif option.nargs == '?': group_text += ' ' elif option.type is not None: - group_text += f' {option.type.__name__.upper()}' + if hasattr(option.type, '__class__'): + group_text += f' {option.type.__class__.__name__.upper()}' + else: + group_text += f' {option.type.__name__.upper()}' elif option.default is None: group_text += f' {option.dest.upper()}' # Any options that haven't tripped one of the conditions above should be a store_true or store_false, and From 4da90a068c370eeab1cef2604c807f4adf1dcc64 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 19 Feb 2024 10:58:26 +1100 Subject: [PATCH 34/75] Python API: Multiple API/CLI changes - Rename app.read_dwgrad_import_options() and app.read_dwgrad_export_options() to just app.dwgrad_import_options() and app.dwgrad_export_options(). - Change these functions to return lists of strings rather than strings. - Fix check_output() functions for output file and directory CLI types. - Fix app.DirectoryOut.mkdir() function. - Remove manual handling of gradient import options in dwi2response manual; replace with app.dwgrad_import_options(). --- bin/dwi2mask | 9 ++++--- bin/dwi2response | 22 ++++++++++------- bin/dwibiascorrect | 4 +-- bin/dwibiasnormmask | 9 ++++--- bin/dwifslpreproc | 17 +++++-------- bin/dwigradcheck | 12 ++++++--- bin/dwishellmath | 6 +++-- bin/population_template | 2 +- lib/mrtrix3/app.py | 37 +++++++++++++--------------- lib/mrtrix3/dwi2mask/mtnorm.py | 3 ++- lib/mrtrix3/dwibiascorrect/mtnorm.py | 4 +-- lib/mrtrix3/dwinormalise/manual.py | 28 ++++++++++----------- lib/mrtrix3/dwinormalise/mtnorm.py | 8 +++--- lib/mrtrix3/image.py | 2 +- lib/mrtrix3/run.py | 2 +- 15 files changed, 87 insertions(+), 78 deletions(-) diff --git a/bin/dwi2mask b/bin/dwi2mask index 703575731e..a1b11031aa 100755 --- a/bin/dwi2mask +++ b/bin/dwi2mask @@ -46,14 +46,15 @@ def execute(): #pylint: disable=unused-variable input_header = image.Header(app.ARGS.input) image.check_3d_nonunity(input_header) - grad_import_option = app.read_dwgrad_import_options() + grad_import_option = app.dwgrad_import_options() if not grad_import_option and 'dw_scheme' not in input_header.keyval(): raise MRtrixError('Script requires diffusion gradient table: ' 'either in image header, or using -grad / -fslgrad option') app.activate_scratch_dir() # Get input data into the scratch directory - run.command(f'mrconvert {app.ARGS.input} input.mif -strides 0,0,0,1 {grad_import_option}', + run.command(['mrconvert', app.ARGS.input, 'input.mif', '-strides', '0,0,0,1'] + + grad_import_option, preserve_pipes=True) # Generate a mean b=0 image (common task in many algorithms) @@ -84,8 +85,8 @@ def execute(): #pylint: disable=unused-variable # the DWI data are valid # (want to ensure that no algorithm includes any voxels where # there is no valid DWI data, regardless of how they operate) - run.command(f'mrcalc {mask_path} input_pos_mask.mif -mult - | ' - f'mrconvert - {app.ARGS.output} -strides {strides} -datatype bit', + run.command(['mrcalc', mask_path, 'input_pos_mask.mif', '-mult', '-', '|', + 'mrconvert', '-', app.ARGS.output, '-strides', strides, '-datatype', 'bit'], mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/bin/dwi2response b/bin/dwi2response index 94c7b0ca59..8c8e8687d6 100755 --- a/bin/dwi2response +++ b/bin/dwi2response @@ -78,16 +78,16 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Value(s) of lmax must be even') if alg.NEEDS_SINGLE_SHELL and not len(app.ARGS.lmax) == 1: raise MRtrixError('Can only specify a single lmax value for single-shell algorithms') - shells_option = '' + shells_option = [] if app.ARGS.shells: if alg.NEEDS_SINGLE_SHELL and len(app.ARGS.shells) != 1: raise MRtrixError('Can only specify a single b-value shell for single-shell algorithms') - shells_option = ' -shells ' + ','.join(map(str,app.ARGS.shells)) - singleshell_option = '' + shells_option = ['-shells', ','.join(map(str,app.ARGS.shells))] + singleshell_option = [] if alg.NEEDS_SINGLE_SHELL: - singleshell_option = ' -singleshell -no_bzero' + singleshell_option = ['-singleshell', '-no_bzero'] - grad_import_option = app.read_dwgrad_import_options() + grad_import_option = app.dwgrad_import_options() if not grad_import_option and 'dw_scheme' not in image.Header(app.ARGS.input).keyval(): raise MRtrixError('Script requires diffusion gradient table: ' 'either in image header, or using -grad / -fslgrad option') @@ -96,18 +96,22 @@ def execute(): #pylint: disable=unused-variable # Get standard input data into the scratch directory if alg.NEEDS_SINGLE_SHELL or shells_option: app.console(f'Importing DWI data ({app.ARGS.input}) and selecting b-values...') - run.command(f'mrconvert {app.ARGS.input} - -strides 0,0,0,1 {grad_import_option} | ' - f'dwiextract - dwi.mif {shells_option} {singleshell_option}', + run.command(['mrconvert', app.ARGS.input, '-', '-strides', '0,0,0,1'] + + grad_import_option + + ['|', 'dwiextract', '-', 'dwi.mif'] + + shells_option + + singleshell_option, show=False, preserve_pipes=True) else: # Don't discard b=0 in multi-shell algorithms app.console(f'Importing DWI data ({app.ARGS.input})...') - run.command(f'mrconvert {app.ARGS.input} dwi.mif -strides 0,0,0,1 {grad_import_option}', + run.command(['mrconvert', app.ARGS.input, 'dwi.mif', '-strides', '0,0,0,1'] + + grad_import_option, show=False, preserve_pipes=True) if app.ARGS.mask: app.console(f'Importing mask ({app.ARGS.mask})...') - run.command(f'mrconvert {app.ARGS.mask} mask.mif -datatype bit', + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], show=False, preserve_pipes=True) diff --git a/bin/dwibiascorrect b/bin/dwibiascorrect index 910df34216..ffc6015790 100755 --- a/bin/dwibiascorrect +++ b/bin/dwibiascorrect @@ -50,8 +50,8 @@ def execute(): #pylint: disable=unused-variable alg = algorithm.get_module(app.ARGS.algorithm) app.activate_scratch_dir() - grad_import_option = app.read_dwgrad_import_options() - run.command(f'mrconvert {app.ARGS.input} in.mif {grad_import_option}', + run.command(['mrconvert', app.ARGS.input, 'in.mif'] + + app.dwgrad_import_options(), preserve_pipes=True) if app.ARGS.mask: run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], diff --git a/bin/dwibiasnormmask b/bin/dwibiasnormmask index 86762d9e47..8f1ea152e3 100755 --- a/bin/dwibiasnormmask +++ b/bin/dwibiasnormmask @@ -172,10 +172,13 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError(f'{software} command "{command}" not found; cannot use for internal mask calculations') app.activate_scratch_dir() - grad_import_option = app.read_dwgrad_import_options() - run.command(f'mrconvert {app.ARGS.input} input.mif {grad_import_option}') + run.command(['mrconvert', app.ARGS.input, 'input.mif'] + + app.dwgrad_import_options(), + preserve_pipes=True) if app.ARGS.init_mask: - run.command(f'mrconvert {app.ARGS.init_mask} dwi_mask_init.mif -datatype bit') + run.command(['mrconvert', app.ARGS.init_mask, 'dwi_mask_init.mif', + '-datatype', 'bit'], + preserve_pipes=True) diff --git a/bin/dwifslpreproc b/bin/dwifslpreproc index f600e8af70..313c4ed385 100755 --- a/bin/dwifslpreproc +++ b/bin/dwifslpreproc @@ -288,10 +288,6 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Could not find any version of FSL eddy command') fsl_suffix = fsl.suffix() - # Export the gradient table to the path requested by the user if necessary - grad_export_option = app.read_dwgrad_export_options() - - eddyqc_files = [ 'eddy_parameters', 'eddy_movement_rms', 'eddy_restricted_movement_rms', \ 'eddy_post_eddy_shell_alignment_parameters', 'eddy_post_eddy_shell_PE_translation_parameters', \ 'eddy_outlier_report', 'eddy_outlier_map', 'eddy_outlier_n_stdev_map', 'eddy_outlier_n_sqr_stdev_map', \ @@ -377,12 +373,10 @@ def execute(): #pylint: disable=unused-variable # Convert all input images into MRtrix format and store in scratch directory first app.activate_scratch_dir() - grad_import_option = app.read_dwgrad_import_options() - json_import_option = '' - if app.ARGS.json_import: - json_import_option = f' -json_import {app.ARGS.json_import}' - json_export_option = ' -json_export dwi.json' - run.command(f'mrconvert {app.ARGS.input} dwi.mif {grad_import_option} {json_import_option} {json_export_option}', + run.command(['mrconvert', app.ARGS.input, 'dwi.mif'] + + (['-json_import', app.ARGS.json_import] if app.ARGS.json_import else []) + + ['-json_export', 'dwi.json'] + + app.dwgrad_import_options(), preserve_pipes=True) if app.ARGS.se_epi: image.check_3d_nonunity(app.ARGS.se_epi) @@ -1575,7 +1569,8 @@ def execute(): #pylint: disable=unused-variable # Finish! - run.command(f'mrconvert result.mif {app.ARGS.output} {grad_export_option}', + run.command(['mrconvert', 'result.mif', app.ARGS.output] + + app.dwgrad_export_options(), mrconvert_keyval='output.json', force=app.FORCE_OVERWRITE) diff --git a/bin/dwigradcheck b/bin/dwigradcheck index 9b32ba819a..d6efe5226f 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -206,16 +206,20 @@ def execute(): #pylint: disable=unused-variable # If requested, extract what has been detected as the best gradient table, and # export it in the format requested by the user - grad_export_option = app.read_dwgrad_export_options() + grad_export_option = app.dwgrad_export_options() if grad_export_option: best = lengths[0] perm = ''.join(map(str, best[2])) suffix = f'_flip{best[1]}_perm{perm}_{best[3]}' if best[3] == 'scanner': - grad_import_option = f' -grad grad{suffix}.b' + grad_import_option = ['-grad', 'grad{suffix}.b'] elif best[3] == 'image': - grad_import_option = f' -fslgrad bvecs{suffix} bvals' - run.command(f'mrinfo data.mif {grad_import_option} {grad_export_option}', + grad_import_option = ['-fslgrad', 'bvecs{suffix}', 'bvals'] + else: + assert False + run.command(['mrinfo', 'data.mif'] + + grad_import_option + + grad_export_option, force=app.FORCE_OVERWRITE) diff --git a/bin/dwishellmath b/bin/dwishellmath index 76cfcf7e2d..9d1a192e65 100755 --- a/bin/dwishellmath +++ b/bin/dwishellmath @@ -52,12 +52,14 @@ def execute(): #pylint: disable=unused-variable dwi_header = image.Header(app.ARGS.input) if len(dwi_header.size()) != 4: raise MRtrixError('Input image must be a 4D image') - gradimport = app.read_dwgrad_import_options() + gradimport = app.dwgrad_import_options() if not gradimport and 'dw_scheme' not in dwi_header.keyval(): raise MRtrixError('No diffusion gradient table provided, and none present in image header') # import data and gradient table app.activate_scratch_dir() - run.command(f'mrconvert {app.ARGS.input} in.mif {gradimport} -strides 0,0,0,1', + run.command(['mrconvert', app.ARGS.input, 'in.mif', + '-strides', '0,0,0,1'] + + gradimport, preserve_pipes=True) # run per-shell operations files = [] diff --git a/bin/population_template b/bin/population_template index d7c76bb820..70d3e9e39f 100755 --- a/bin/population_template +++ b/bin/population_template @@ -471,7 +471,7 @@ class Contrasts: self.suff = [f'_c{c}' for c in map(str, range(n_contrasts))] self.names = [os.path.relpath(f, os.path.commonprefix(app.ARGS.input_dir)) for f in app.ARGS.input_dir] - self.templates_out = [t for t in app.ARGS.template] + self.templates_out = list(app.ARGS.template) self.mc_weight_initial_alignment = [None for _ in range(self.n_contrasts)] self.mc_weight_rigid = [None for _ in range(self.n_contrasts)] diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index 11daa9e4c2..f6580cec5c 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -582,7 +582,7 @@ class _UserFileOutPath(UserPath): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) def check_output(self): - if self.exists: + if self.exists(): if FORCE_OVERWRITE: warn(f'Output file path "{str(self)}" already exists; ' 'will be overwritten at script completion') @@ -594,16 +594,16 @@ class _UserDirOutPath(UserPath): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) def check_output(self): - if self.exists: + if self.exists(): if FORCE_OVERWRITE: warn(f'Output directory path "{str(self)}" already exists; ' 'will be overwritten at script completion') else: raise MRtrixError(f'Output directory "{str(self)}" already exists ' '(use -force to overwrite)') - def mkdir(self, **kwargs): - # Always force parents=True for user-specified path - parents = kwargs.pop('parents', True) + # Force parents=True for user-specified path + # Force exist_ok=False for user-specified path + def mkdir(self, mode=0o777): while True: if FORCE_OVERWRITE: try: @@ -611,11 +611,11 @@ def mkdir(self, **kwargs): except OSError: pass try: - super().mkdir(parents=parents, **kwargs) + super().mkdir(mode, parents=True, exist_ok=False) return except FileExistsError: if not FORCE_OVERWRITE: - raise MRtrixError(f'Output directory "{str(self)}" already exists ' + raise MRtrixError(f'Output directory "{str(self)}" already exists ' # pylint: disable=raise-missing-from '(use -force to override)') @@ -652,7 +652,7 @@ def __call__(self, input_value): def _typestring(): return 'BOOL' - def Int(min_value=None, max_value=None): # pylint: disable=invalid-name + def Int(min_value=None, max_value=None): # pylint: disable=invalid-name,no-self-argument assert min_value is None or isinstance(min_value, int) assert max_value is None or isinstance(max_value, int) assert min_value is None or max_value is None or max_value >= min_value @@ -672,7 +672,7 @@ def _typestring(): return f'INT {-sys.maxsize - 1 if min_value is None else min_value} {sys.maxsize if max_value is None else max_value}' return IntBounded() - def Float(min_value=None, max_value=None): # pylint: disable=invalid-name + def Float(min_value=None, max_value=None): # pylint: disable=invalid-name,no-self-argument assert min_value is None or isinstance(min_value, float) assert max_value is None or isinstance(max_value, float) assert min_value is None or max_value is None or max_value >= min_value @@ -1447,14 +1447,13 @@ def add_dwgrad_import_options(cmdline): #pylint: disable=unused-variable help='Provide the diffusion gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'grad', 'fslgrad' ] ) -# TODO Change these to yield lists rather than strings -def read_dwgrad_import_options(): #pylint: disable=unused-variable +def dwgrad_import_options(): #pylint: disable=unused-variable assert ARGS if ARGS.grad: - return f' -grad {ARGS.grad}' + return ['-grad', ARGS.grad] if ARGS.fslgrad: - return f' -fslgrad {ARGS.fslgrad[0]} {ARGS.fslgrad[1]}' - return '' + return ['-fslgrad', ARGS.fslgrad[0], ARGS.fslgrad[1]] + return [] @@ -1472,15 +1471,13 @@ def add_dwgrad_export_options(cmdline): #pylint: disable=unused-variable help='Export the final gradient table in FSL bvecs/bvals format') cmdline.flag_mutually_exclusive_options( [ 'export_grad_mrtrix', 'export_grad_fsl' ] ) - - -def read_dwgrad_export_options(): #pylint: disable=unused-variable +def dwgrad_export_options(): #pylint: disable=unused-variable assert ARGS if ARGS.export_grad_mrtrix: - return f' -export_grad_mrtrix {ARGS.export_grad_mrtrix}' + return ['-export_grad_mrtrix', ARGS.export_grad_mrtrix] if ARGS.export_grad_fsl: - return f' -export_grad_fsl {ARGS.export_grad_fsl[0]} {ARGS.export_grad_fsl[1]}' - return '' + return ['-export_grad_fsl', ARGS.export_grad_fsl[0], ARGS.export_grad_fsl[1]] + return [] diff --git a/lib/mrtrix3/dwi2mask/mtnorm.py b/lib/mrtrix3/dwi2mask/mtnorm.py index e51825951e..a143f081ee 100644 --- a/lib/mrtrix3/dwi2mask/mtnorm.py +++ b/lib/mrtrix3/dwi2mask/mtnorm.py @@ -92,7 +92,8 @@ def execute(): #pylint: disable=unused-variable if len(lmax) not in [2, 3]: raise MRtrixError('Length of lmax vector expected to be either 2 or 3') if app.ARGS.init_mask: - run.command(['mrconvert', app.ARGS.init_mask, 'init_mask.mif', '-datatype', 'bit']) + run.command(['mrconvert', app.ARGS.init_mask, 'init_mask.mif', '-datatype', 'bit'], + preserve_pipes=True) # Determine whether we are working with single-shell or multi-shell data bvalues = [ diff --git a/lib/mrtrix3/dwibiascorrect/mtnorm.py b/lib/mrtrix3/dwibiascorrect/mtnorm.py index acf4903bf9..9934cb6f9e 100644 --- a/lib/mrtrix3/dwibiascorrect/mtnorm.py +++ b/lib/mrtrix3/dwibiascorrect/mtnorm.py @@ -97,9 +97,9 @@ def __init__(self, name): tissues = [Tissue('WM'), Tissue('GM'), Tissue('CSF')] - # TODO Fix + dwi2response_mask_option = ['-mask', 'mask.mif'] if app.ARGS.mask else [] run.command(['dwi2response', 'dhollander', 'in.mif', [tissue.rffile for tissue in tissues]] - .extend(['-mask', 'mask.mif'] if app.ARGS.mask else [])) + + dwi2response_mask_option) # Immediately remove GM if we can't deal with it if not multishell: diff --git a/lib/mrtrix3/dwinormalise/manual.py b/lib/mrtrix3/dwinormalise/manual.py index 7e7dc84a8d..5540b9e01e 100644 --- a/lib/mrtrix3/dwinormalise/manual.py +++ b/lib/mrtrix3/dwinormalise/manual.py @@ -53,16 +53,13 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable - grad_option = '' - if app.ARGS.grad: - grad_option = f' -grad {app.ARGS.grad}' - elif app.ARGS.fslgrad: - grad_option = f' -fslgrad {app.ARGS.fslgrad[0]} {app.ARGS.fslgrad[1]}' - + grad_option = app.dwgrad_import_options() if app.ARGS.percentile: - intensities = [float(value) for value in run.command(f'dwiextract {app.ARGS.input_dwi} {grad_option} -bzero - | ' - f'mrmath - mean - -axis 3 | ' - f'mrdump - -mask {app.ARGS.input_mask}', + intensities = [float(value) for value in run.command(['dwiextract', app.ARGS.input_dwi] + + grad_option + + ['-bzero', '-', '|', + 'mrmath', '-', 'mean', '-', '-axis', '3', '|', + 'mrdump', '-', '-mask', app.ARGS.input_mask], preserve_pipes=True).stdout.splitlines()] intensities = sorted(intensities) float_index = 0.01 * app.ARGS.percentile * len(intensities) @@ -73,14 +70,17 @@ def execute(): #pylint: disable=unused-variable interp_mu = float_index - float(lower_index) reference_value = (1.0-interp_mu)*intensities[lower_index] + interp_mu*intensities[lower_index+1] else: - reference_value = float(run.command(f'dwiextract {app.ARGS.input_dwi} {grad_option} -bzero - | ' - f'mrmath - mean - -axis 3 | ' - f'mrstats - -mask {app.ARGS.input_mask} -output median', + reference_value = float(run.command(['dwiextract', app.ARGS.input_dwi] + + grad_option + + ['-bzero', '-', '|', + 'mrmath', '-', 'mean', '-', '-axis', '3', '|', + 'mrstats', '-', '-mask', app.ARGS.input_mask, '-output', 'median'], preserve_pipes=True).stdout) multiplier = app.ARGS.intensity / reference_value - run.command(f'mrcalc {app.ARGS.input_dwi} {multiplier} -mult - | ' - f'mrconvert - {app.ARGS.output_dwi} {grad_option}', + run.command(['mrcalc', app.ARGS.input_dwi, str(multiplier), '-mult', '-', '|', + 'mrconvert', '-', app.ARGS.output_dwi] + + grad_option, mrconvert_keyval=app.ARGS.input_dwi, force=app.FORCE_OVERWRITE, preserve_pipes=True) diff --git a/lib/mrtrix3/dwinormalise/mtnorm.py b/lib/mrtrix3/dwinormalise/mtnorm.py index 574152dd3e..1765ce089f 100644 --- a/lib/mrtrix3/dwinormalise/mtnorm.py +++ b/lib/mrtrix3/dwinormalise/mtnorm.py @@ -97,11 +97,13 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError('Length of lmax vector expected to be either 2 or 3') # Get input data into the scratch directory - grad_option = app.read_dwgrad_import_options().split(' ') app.activate_scratch_dir() - run.command(['mrconvert', app.ARGS.input, 'input.mif'] + grad_option) + run.command(['mrconvert', app.ARGS.input, 'input.mif'] + + app.dwgrad_import_options(), + preserve_pipes=True) if app.ARGS.mask: - run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit']) + run.command(['mrconvert', app.ARGS.mask, 'mask.mif', '-datatype', 'bit'], + preserve_pipes=True) # Make sure we have a valid mask available if app.ARGS.mask: diff --git a/lib/mrtrix3/image.py b/lib/mrtrix3/image.py index 06431ebff3..f4b0bc946a 100644 --- a/lib/mrtrix3/image.py +++ b/lib/mrtrix3/image.py @@ -129,7 +129,7 @@ def axis2dir(string): #pylint: disable=unused-variable def check_3d_nonunity(image_in): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=import-outside-toplevel if not isinstance(image_in, Header): - if not isinstance(image_in, str) and not isinstance(image_in, app._FilesystemPath): + if not isinstance(image_in, str) and not isinstance(image_in, pathlib.Path): raise MRtrixError(f'Error trying to test "{image_in}": ' 'Not an image header or file path') image_in = Header(image_in) diff --git a/lib/mrtrix3/run.py b/lib/mrtrix3/run.py index 3ae730a7e8..aa06444162 100644 --- a/lib/mrtrix3/run.py +++ b/lib/mrtrix3/run.py @@ -350,7 +350,7 @@ def quote_nonpipe(item): if cmdstack[-1][0] != 'mrconvert': raise TypeError('Argument "mrconvert_keyval=" can only be used ' 'if the mrconvert command is being invoked') - if isinstance(mrconvert_keyval, app._FilesystemPath): + if isinstance(mrconvert_keyval, pathlib.Path): cmdstack[-1].extend([ '-copy_properties', str(mrconvert_keyval) ]) else: assert not (mrconvert_keyval[0] in [ '\'', '"' ] or mrconvert_keyval[-1] in [ '\'', '"' ]) From 4c747590155854b6c259886e6d912fef96bed771 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 19 Feb 2024 11:57:15 +1100 Subject: [PATCH 35/75] Testing: Verify piped image support for Python scripts --- lib/mrtrix3/dwi2mask/b02template.py | 12 ++++++++---- lib/mrtrix3/dwi2mask/mtnorm.py | 1 + lib/mrtrix3/dwi2mask/synthstrip.py | 1 + testing/scripts/tests/5ttgen | 7 +++++-- testing/scripts/tests/dwi2mask | 5 +++++ testing/scripts/tests/dwi2response | 10 ++++++++-- testing/scripts/tests/dwibiascorrect | 3 +++ testing/scripts/tests/dwibiasnormmask | 3 +++ testing/scripts/tests/dwicat | 2 ++ testing/scripts/tests/dwifslpreproc | 2 ++ testing/scripts/tests/dwigradcheck | 3 +++ testing/scripts/tests/dwinormalise | 6 ++++++ testing/scripts/tests/dwishellmath | 1 + testing/scripts/tests/labelsgmfix | 2 ++ testing/scripts/tests/population_template | 2 ++ 15 files changed, 52 insertions(+), 8 deletions(-) diff --git a/lib/mrtrix3/dwi2mask/b02template.py b/lib/mrtrix3/dwi2mask/b02template.py index b8a5da1f11..76b42a922a 100644 --- a/lib/mrtrix3/dwi2mask/b02template.py +++ b/lib/mrtrix3/dwi2mask/b02template.py @@ -226,14 +226,18 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.template: run.command(['mrconvert', app.ARGS.template[0], 'template_image.nii', - '-strides', '+1,+2,+3']) + '-strides', '+1,+2,+3'], + preserve_pipes=True) run.command(['mrconvert', app.ARGS.template[1], 'template_mask.nii', - '-strides', '+1,+2,+3', '-datatype', 'uint8']) + '-strides', '+1,+2,+3', '-datatype', 'uint8'], + preserve_pipes=True) elif all(item in CONFIG for item in ['Dwi2maskTemplateImage', 'Dwi2maskTemplateMask']): run.command(['mrconvert', CONFIG['Dwi2maskTemplateImage'], 'template_image.nii', - '-strides', '+1,+2,+3']) + '-strides', '+1,+2,+3'], + preserve_pipes=True) run.command(['mrconvert', CONFIG['Dwi2maskTemplateMask'], 'template_mask.nii', - '-strides', '+1,+2,+3', '-datatype', 'uint8']) + '-strides', '+1,+2,+3', '-datatype', 'uint8'], + preserve_pipes=True) else: raise MRtrixError('No template image information available from ' 'either command-line or MRtrix configuration file(s)') diff --git a/lib/mrtrix3/dwi2mask/mtnorm.py b/lib/mrtrix3/dwi2mask/mtnorm.py index a143f081ee..d0348bc50c 100644 --- a/lib/mrtrix3/dwi2mask/mtnorm.py +++ b/lib/mrtrix3/dwi2mask/mtnorm.py @@ -145,6 +145,7 @@ def __init__(self, name): if app.ARGS.tissuesum: run.command(['mrconvert', tissue_sum_image, app.ARGS.tissuesum], mrconvert_keyval=app.ARGS.input, + preserve_pipes=True, force=app.FORCE_OVERWRITE) return mask_image diff --git a/lib/mrtrix3/dwi2mask/synthstrip.py b/lib/mrtrix3/dwi2mask/synthstrip.py index 5d6b29a56e..603620ed3b 100644 --- a/lib/mrtrix3/dwi2mask/synthstrip.py +++ b/lib/mrtrix3/dwi2mask/synthstrip.py @@ -93,5 +93,6 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.stripped: run.command(['mrconvert', stripped_file, app.ARGS.stripped], mrconvert_keyval=app.ARGS.input, + preserve_pipes=True, force=app.FORCE_OVERWRITE) return output_file diff --git a/testing/scripts/tests/5ttgen b/testing/scripts/tests/5ttgen index 8b9e99e2bd..9c62b6564c 100644 --- a/testing/scripts/tests/5ttgen +++ b/testing/scripts/tests/5ttgen @@ -1,14 +1,17 @@ mkdir -p ../tmp/5ttgen/freesurfer && 5ttgen freesurfer BIDS/sub-01/anat/aparc+aseg.mgz ../tmp/5ttgen/freesurfer/default.mif -force && testing_diff_image ../tmp/5ttgen/freesurfer/default.mif 5ttgen/freesurfer/default.mif.gz +mrconvert BIDS/sub-01/anat/aparc+aseg.mgz - | 5ttgen freesurfer - - && testing_diff_image - 5ttgen/freesurfer/default.mif.gz 5ttgen freesurfer BIDS/sub-01/anat/aparc+aseg.mgz ../tmp/5ttgen/freesurfer/nocrop.mif -nocrop -force && testing_diff_image ../tmp/5ttgen/freesurfer/nocrop.mif 5ttgen/freesurfer/nocrop.mif.gz 5ttgen freesurfer BIDS/sub-01/anat/aparc+aseg.mgz ../tmp/5ttgen/freesurfer/sgm_amyg_hipp.mif -sgm_amyg_hipp -force && testing_diff_image ../tmp/5ttgen/freesurfer/sgm_amyg_hipp.mif 5ttgen/freesurfer/sgm_amyg_hipp.mif.gz mkdir -p ../tmp/5ttgen/fsl && 5ttgen fsl BIDS/sub-01/anat/sub-01_T1w.nii.gz ../tmp/5ttgen/fsl/default.mif -force # && testing_diff_header ../tmp/5ttgen/fsl/default.mif 5ttgen/fsl/default.mif.gz +mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz - | 5ttgen fsl - - | mrconvert - ../tmp/5ttgen/fsl/default.mif -force 5ttgen fsl BIDS/sub-01/anat/sub-01_T1w.nii.gz ../tmp/5ttgen/fsl/nocrop.mif -nocrop -force && testing_diff_header ../tmp/5ttgen/fsl/nocrop.mif 5ttgen/fsl/nocrop.mif.gz 5ttgen fsl BIDS/sub-01/anat/sub-01_T1w.nii.gz ../tmp/5ttgen/fsl/sgm_amyg_hipp.mif -sgm_amyg_hipp -force # && testing_diff_header ../tmp/5ttgen/fsl/sgm_amyg_hipp.mif 5ttgen/fsl/sgm_amyg_hipp.mif.gz 5ttgen fsl BIDS/sub-01/anat/sub-01_T1w.nii.gz ../tmp/5ttgen/fsl/masked.mif -mask BIDS/sub-01/anat/sub-01_brainmask.nii.gz -force && testing_diff_header ../tmp/5ttgen/fsl/masked.mif 5ttgen/fsl/masked.mif.gz mrcalc BIDS/sub-01/anat/sub-01_T1w.nii.gz BIDS/sub-01/anat/sub-01_brainmask.nii.gz -mult tmp1.mif -force && 5ttgen fsl tmp1.mif ../tmp/5ttgen/fsl/premasked.mif -premasked -force && testing_diff_header ../tmp/5ttgen/fsl/premasked.mif 5ttgen/fsl/masked.mif.gz mkdir -p ../tmp/5ttgen/hsvs && 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/default.mif -force && testing_diff_header ../tmp/5ttgen/hsvs/default.mif 5ttgen/hsvs/default.mif.gz +5ttgen hsvs freesurfer/sub-01 - && testing_diff_header - 5ttgen/hsvs/default.mif.gz 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/white_stem.mif -white_stem -force && testing_diff_header ../tmp/5ttgen/hsvs/white_stem.mif 5ttgen/hsvs/white_stem.mif.gz 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/modules.mif -hippocampi subfields -thalami nuclei -force && testing_diff_header ../tmp/5ttgen/hsvs/modules.mif 5ttgen/hsvs/modules.mif.gz -5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/first.mif -hippocampi first -thalami first -force && testing_diff_header ../tmp/5ttgen/hsvs/first.mif 5ttgen/hsvs/first.mif.gz -5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/aseg.mif -hippocampi aseg -thalami aseg -force && testing_diff_header ../tmp/5ttgen/hsvs/aseg.mif 5ttgen/hsvs/aseg.mif.gz +5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/first.mif -hippocampi first -thalami first -force && testing_diff_header ../tmp/5ttgen/hsvs/first.mif 5ttgen/hsvs/first.mif.gz +5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/aseg.mif -hippocampi aseg -thalami aseg -force && testing_diff_header ../tmp/5ttgen/hsvs/aseg.mif 5ttgen/hsvs/aseg.mif.gz 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/template.mif -template BIDS/sub-01/anat/sub-01_T1w.nii.gz -force && testing_diff_header ../tmp/5ttgen/hsvs/template.mif 5ttgen/hsvs/template.mif.gz diff --git a/testing/scripts/tests/dwi2mask b/testing/scripts/tests/dwi2mask index 367342ba99..d98f3dc403 100644 --- a/testing/scripts/tests/dwi2mask +++ b/testing/scripts/tests/dwi2mask @@ -4,6 +4,8 @@ dwi2mask ants tmp-sub-01_dwi.mif ../tmp/dwi2mask/ants_default.mif -template dwi2 dwi2mask ants tmp-sub-01_dwi.mif ../tmp/dwi2mask/ants_config.mif -config Dwi2maskTemplateImage dwi2mask/template_image.mif.gz -config Dwi2maskTemplateMask dwi2mask/template_mask.mif.gz -force dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsfull_default.mif -software antsfull -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -force dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsquick_default.mif -software antsquick -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -force +mrconvert dwi2mask/template_image.mif.gz - | dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsquick_default.mif -software antsquick -template - dwi2mask/template_mask.mif.gz -force +mrconvert dwi2mask/template_mask.mif.gz - | dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsquick_default.mif -software antsquick -template dwi2mask/template_image.mif.gz - -force dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsfull_cmdline.mif -software antsfull -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -ants_options "--use-histogram-matching 1 --initial-moving-transform [template_image.nii,bzero.nii,1] --transform Rigid[0.1] --metric MI[template_image.nii,bzero.nii,1,32,Regular,0.25] --convergence 1000x500x250x100 --smoothing-sigmas 3x2x1x0 --shrink-factors 8x4x2x1" -force dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsfull_config.mif -software antsfull -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -config Dwi2maskTemplateAntsFullOptions "--use-histogram-matching 1 --initial-moving-transform [template_image.nii,bzero.nii,1] --transform Rigid[0.1] --metric MI[template_image.nii,bzero.nii,1,32,Regular,0.25] --convergence 1000x500x250x100 --smoothing-sigmas 3x2x1x0 --shrink-factors 8x4x2x1" -force dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsfull_file1.mif -software antsfull -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -ants_options dwi2mask/config_antsfull_1.txt -force @@ -21,12 +23,15 @@ dwi2mask fslbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/fslbet_options.mif -bet_f 0.5 dwi2mask fslbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/fslbet_rescale.mif -rescale -force dwi2mask hdbet tmp-sub-01_dwi.mif ../tmp/dwi2mask/hdbet.mif -force dwi2mask legacy tmp-sub-01_dwi.mif ../tmp/dwi2mask/legacy.mif -force && testing_diff_image ../tmp/dwi2mask/legacy.mif dwi2mask/legacy.mif.gz +mrconvert tmp-sub-01_dwi.mif - | dwi2mask legacy - - | testing_diff_image - dwi2mask/legacy.mif.gz dwi2mask mean tmp-sub-01_dwi.mif ../tmp/dwi2mask/mean.mif -force && testing_diff_image ../tmp/dwi2mask/mean.mif dwi2mask/mean.mif.gz dwi2mask mtnorm tmp-sub-01_dwi.mif ../tmp/dwi2mask/mtnorm_default_mask.mif -tissuesum ../tmp/dwi2mask/mtnorm_default_tissuesum.mif -force && testing_diff_image ../tmp/dwi2mask/mtnorm_default_mask.mif dwi2mask/mtnorm_default_mask.mif.gz && testing_diff_image ../tmp/dwi2mask/mtnorm_default_tissuesum.mif dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 +dwi2mask mtnorm tmp-sub-01_dwi.mif ../tmp/dwi2mask/mtnorm_default_mask.mif -tissuesum - | testing_diff_image - dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 dwi2mask mtnorm tmp-sub-01_dwi.mif -init_mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz ../tmp/dwi2mask/mtnorm_initmask_mask.mif -tissuesum ../tmp/dwi2mask/mtnorm_initmask_tissuesum.mif -force && testing_diff_image ../tmp/dwi2mask/mtnorm_initmask_mask.mif dwi2mask/mtnorm_initmask_mask.mif.gz && testing_diff_image ../tmp/dwi2mask/mtnorm_initmask_tissuesum.mif dwi2mask/mtnorm_initmask_tissuesum.mif.gz -abs 1e-5 dwi2mask mtnorm tmp-sub-01_dwi.mif -lmax 6,0,0 ../tmp/dwi2mask/mtnorm_lmax600_mask.mif -tissuesum ../tmp/dwi2mask/mtnorm_lmax600_tissuesum.mif -force && testing_diff_image ../tmp/dwi2mask/mtnorm_lmax600_mask.mif dwi2mask/mtnorm_lmax600_mask.mif.gz && testing_diff_image ../tmp/dwi2mask/mtnorm_lmax600_tissuesum.mif dwi2mask/mtnorm_lmax600_tissuesum.mif.gz -abs 1e-5 dwi2mask synthstrip tmp-sub-01_dwi.mif ../tmp/dwi2mask/synthstrip_default.mif -force dwi2mask synthstrip tmp-sub-01_dwi.mif ../tmp/dwi2mask/synthstrip_options.mif -stripped ../tmp/dwi2mask/synthstrip_stripped.mif -gpu -nocsf -border 0 -force +dwi2mask synthstrip tmp-sub-01_dwi.mif ../tmp/dwi2mask/synthstrip_options.mif -stripped - -gpu -nocsf -border 0 | mrconvert - ../tmp/dwi2mask/synthstrip_stripped.mif -force dwi2mask trace tmp-sub-01_dwi.mif ../tmp/dwi2mask/trace_default.mif -force && testing_diff_image ../tmp/dwi2mask/trace_default.mif dwi2mask/trace_default.mif.gz dwi2mask trace tmp-sub-01_dwi.mif ../tmp/dwi2mask/trace_iterative.mif -iterative -force && testing_diff_image ../tmp/dwi2mask/trace_iterative.mif dwi2mask/trace_iterative.mif.gz diff --git a/testing/scripts/tests/dwi2response b/testing/scripts/tests/dwi2response index 76651900d5..9b633ac9b5 100644 --- a/testing/scripts/tests/dwi2response +++ b/testing/scripts/tests/dwi2response @@ -1,13 +1,16 @@ mkdir -p ../tmp/dwi2response/dhollander && mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval tmp-sub-01_dwi.mif -export_grad_mrtrix tmp-sub-01_dwi.b -strides 0,0,0,1 && dwi2response dhollander tmp-sub-01_dwi.mif ../tmp/dwi2response/dhollander/default_wm.txt ../tmp/dwi2response/dhollander/default_gm.txt ../tmp/dwi2response/dhollander/default_csf.txt -voxels ../tmp/dwi2response/dhollander/default.mif -force && testing_diff_matrix ../tmp/dwi2response/dhollander/default_wm.txt dwi2response/dhollander/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/default_gm.txt dwi2response/dhollander/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/default_csf.txt dwi2response/dhollander/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/default.mif dwi2response/dhollander/default.mif.gz +mrconvert tmp-sub-01_dwi.mif - | dwi2response dhollander - ../tmp/dwi2response/dhollander/default_wm.txt ../tmp/dwi2response/dhollander/default_gm.txt ../tmp/dwi2response/dhollander/default_csf.txt -voxels - -force | testing_diff_image - dwi2response/dhollander/default.mif.gz dwi2response dhollander BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwi2response/dhollander/fslgrad_wm.txt ../tmp/dwi2response/dhollander/fslgrad_gm.txt ../tmp/dwi2response/dhollander/fslgrad_csf.txt -voxels ../tmp/dwi2response/dhollander/fslgrad.mif -force && testing_diff_matrix ../tmp/dwi2response/dhollander/fslgrad_wm.txt dwi2response/dhollander/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/fslgrad_gm.txt dwi2response/dhollander/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/fslgrad_csf.txt dwi2response/dhollander/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/fslgrad.mif dwi2response/dhollander/default.mif.gz dwi2response dhollander BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b ../tmp/dwi2response/dhollander/grad_wm.txt ../tmp/dwi2response/dhollander/grad_gm.txt ../tmp/dwi2response/dhollander/grad_csf.txt -voxels ../tmp/dwi2response/dhollander/grad.mif -force && testing_diff_matrix ../tmp/dwi2response/dhollander/grad_wm.txt dwi2response/dhollander/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/grad_gm.txt dwi2response/dhollander/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/grad_csf.txt dwi2response/dhollander/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/grad.mif dwi2response/dhollander/default.mif.gz dwi2response dhollander tmp-sub-01_dwi.mif ../tmp/dwi2response/dhollander/masked_wm.txt ../tmp/dwi2response/dhollander/masked_gm.txt ../tmp/dwi2response/dhollander/masked_csf.txt -voxels ../tmp/dwi2response/dhollander/masked.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_matrix ../tmp/dwi2response/dhollander/masked_wm.txt dwi2response/dhollander/masked_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/masked_gm.txt dwi2response/dhollander/masked_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/masked_csf.txt dwi2response/dhollander/masked_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/masked.mif dwi2response/dhollander/masked.mif.gz +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | dwi2response dhollander tmp-sub-01_dwi.mif ../tmp/dwi2response/dhollander/masked_wm.txt ../tmp/dwi2response/dhollander/masked_gm.txt ../tmp/dwi2response/dhollander/masked_csf.txt -voxels ../tmp/dwi2response/dhollander/masked.mif -mask - -force && testing_diff_matrix ../tmp/dwi2response/dhollander/masked_wm.txt dwi2response/dhollander/masked_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/masked_gm.txt dwi2response/dhollander/masked_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/masked_csf.txt dwi2response/dhollander/masked_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/masked.mif dwi2response/dhollander/masked.mif.gz dwi2response dhollander tmp-sub-01_dwi.mif ../tmp/dwi2response/dhollander/fa_wm.txt ../tmp/dwi2response/dhollander/fa_gm.txt ../tmp/dwi2response/dhollander/fa_csf.txt -wm_algo fa -voxels ../tmp/dwi2response/dhollander/fa.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_matrix ../tmp/dwi2response/dhollander/fa_wm.txt dwi2response/dhollander/fa_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/fa_gm.txt dwi2response/dhollander/fa_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/fa_csf.txt dwi2response/dhollander/fa_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/fa.mif dwi2response/dhollander/fa.mif.gz dwi2response dhollander tmp-sub-01_dwi.mif ../tmp/dwi2response/dhollander/tax_wm.txt ../tmp/dwi2response/dhollander/tax_gm.txt ../tmp/dwi2response/dhollander/tax_csf.txt -wm_algo tax -voxels ../tmp/dwi2response/dhollander/tax.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_matrix ../tmp/dwi2response/dhollander/tax_wm.txt dwi2response/dhollander/tax_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/tax_gm.txt dwi2response/dhollander/tax_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/tax_csf.txt dwi2response/dhollander/tax_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/tax.mif dwi2response/dhollander/tax.mif.gz dwi2response dhollander tmp-sub-01_dwi.mif ../tmp/dwi2response/dhollander/tournier_wm.txt ../tmp/dwi2response/dhollander/tournier_gm.txt ../tmp/dwi2response/dhollander/tournier_csf.txt -wm_algo tournier -voxels ../tmp/dwi2response/dhollander/tournier.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_matrix ../tmp/dwi2response/dhollander/tournier_wm.txt dwi2response/dhollander/tournier_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/tournier_gm.txt dwi2response/dhollander/tournier_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/tournier_csf.txt dwi2response/dhollander/tournier_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/tournier.mif dwi2response/dhollander/tournier.mif.gz dwi2response dhollander tmp-sub-01_dwi.mif -shells 0,3000 ../tmp/dwi2response/dhollander/shells_wm.txt ../tmp/dwi2response/dhollander/shells_gm.txt ../tmp/dwi2response/dhollander/shells_csf.txt -voxels ../tmp/dwi2response/dhollander/shells.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_matrix ../tmp/dwi2response/dhollander/shells_wm.txt dwi2response/dhollander/shells_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/shells_gm.txt dwi2response/dhollander/shells_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/shells_csf.txt dwi2response/dhollander/shells_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/shells.mif dwi2response/dhollander/shells.mif.gz dwi2response dhollander tmp-sub-01_dwi.mif -lmax 0,2,4,6 ../tmp/dwi2response/dhollander/lmax_wm.txt ../tmp/dwi2response/dhollander/lmax_gm.txt ../tmp/dwi2response/dhollander/lmax_csf.txt -voxels ../tmp/dwi2response/dhollander/lmax.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_matrix ../tmp/dwi2response/dhollander/lmax_wm.txt dwi2response/dhollander/lmax_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/lmax_gm.txt dwi2response/dhollander/lmax_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/dhollander/lmax_csf.txt dwi2response/dhollander/lmax_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/dhollander/lmax.mif dwi2response/dhollander/lmax.mif.gz mkdir -p ../tmp/dwi2response/fa && dwi2response fa tmp-sub-01_dwi.mif ../tmp/dwi2response/fa/default.txt -voxels ../tmp/dwi2response/fa/default.mif -number 20 -force && testing_diff_matrix ../tmp/dwi2response/fa/default.txt dwi2response/fa/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/fa/default.mif dwi2response/fa/default.mif.gz +dwi2response fa tmp-sub-01_dwi.mif ../tmp/dwi2response/fa/default.txt -voxels - -number 20 -force | testing_diff_image - dwi2response/fa/default.mif.gz dwi2response fa BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwi2response/fa/fslgrad.txt -voxels ../tmp/dwi2response/fa/fslgrad.mif -number 20 -force && testing_diff_matrix ../tmp/dwi2response/fa/fslgrad.txt dwi2response/fa/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/fa/fslgrad.mif dwi2response/fa/default.mif.gz dwi2response fa BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b ../tmp/dwi2response/fa/grad.txt -voxels ../tmp/dwi2response/fa/grad.mif -number 20 -force && testing_diff_matrix ../tmp/dwi2response/fa/grad.txt dwi2response/fa/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/fa/grad.mif dwi2response/fa/default.mif.gz dwi2response fa tmp-sub-01_dwi.mif ../tmp/dwi2response/fa/masked.txt -voxels ../tmp/dwi2response/fa/masked.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -number 20 -force && testing_diff_matrix ../tmp/dwi2response/fa/masked.txt dwi2response/fa/masked.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/fa/masked.mif dwi2response/fa/masked.mif.gz @@ -17,25 +20,28 @@ dwi2response fa tmp-sub-01_dwi.mif ../tmp/dwi2response/fa/shells.txt -voxels ../ mkdir -p ../tmp/dwi2response/manual && dwi2response manual tmp-sub-01_dwi.mif dwi2response/fa/default.mif.gz ../tmp/dwi2response/manual/default.txt -force && testing_diff_matrix ../tmp/dwi2response/manual/default.txt dwi2response/manual/default.txt -abs 1e-2 dwi2response manual BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval dwi2response/fa/default.mif.gz ../tmp/dwi2response/manual/fslgrad.txt -force && testing_diff_matrix ../tmp/dwi2response/manual/fslgrad.txt dwi2response/manual/default.txt -abs 1e-2 dwi2response manual BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b dwi2response/fa/default.mif.gz ../tmp/dwi2response/manual/grad.txt -force && testing_diff_matrix ../tmp/dwi2response/manual/grad.txt dwi2response/manual/default.txt -abs 1e-2 -dwi2tensor tmp-sub-01_dwi.mif - -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz | tensor2metric - -vector tmp.mif -force && dwi2response manual tmp-sub-01_dwi.mif dwi2response/fa/default.mif.gz -dirs tmp.mif ../tmp/dwi2response/manual/dirs.txt -force && testing_diff_matrix ../tmp/dwi2response/manual/dirs.txt dwi2response/manual/dirs.txt -abs 1e-2 +dwi2tensor tmp-sub-01_dwi.mif - -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz | tensor2metric - -vector - | dwi2response manual tmp-sub-01_dwi.mif dwi2response/fa/default.mif.gz -dirs - ../tmp/dwi2response/manual/dirs.txt -force && testing_diff_matrix ../tmp/dwi2response/manual/dirs.txt dwi2response/manual/dirs.txt -abs 1e-2 dwi2response manual tmp-sub-01_dwi.mif dwi2response/fa/default.mif.gz ../tmp/dwi2response/manual/lmax.txt -lmax 0,2,4,6 -force && testing_diff_matrix ../tmp/dwi2response/manual/lmax.txt dwi2response/manual/lmax.txt -abs 1e-2 dwi2response manual tmp-sub-01_dwi.mif dwi2response/fa/default.mif.gz ../tmp/dwi2response/manual/shells.txt -shells 0,3000 -force && testing_diff_matrix ../tmp/dwi2response/manual/shells.txt dwi2response/manual/shells.txt -abs 1e-2 mkdir -p ../tmp/dwi2response/msmt_5tt && dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/default_wm.txt ../tmp/dwi2response/msmt_5tt/default_gm.txt ../tmp/dwi2response/msmt_5tt/default_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/default.mif -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/default_wm.txt dwi2response/msmt_5tt/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/default_gm.txt dwi2response/msmt_5tt/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/default_csf.txt dwi2response/msmt_5tt/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/default.mif dwi2response/msmt_5tt/default.mif.gz +mrconvert BIDS/sub-01/anat/sub-01_5TT.nii.gz - | dwi2response msmt_5tt tmp-sub-01_dwi.mif - ../tmp/dwi2response/msmt_5tt/default_wm.txt ../tmp/dwi2response/msmt_5tt/default_gm.txt ../tmp/dwi2response/msmt_5tt/default_csf.txt -voxels - -pvf 0.9 -force | testing_diff_image - dwi2response/msmt_5tt/default.mif.gz dwi2response msmt_5tt BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/fslgrad_wm.txt ../tmp/dwi2response/msmt_5tt/fslgrad_gm.txt ../tmp/dwi2response/msmt_5tt/fslgrad_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/fslgrad.mif -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/fslgrad_wm.txt dwi2response/msmt_5tt/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/fslgrad_gm.txt dwi2response/msmt_5tt/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/fslgrad_csf.txt dwi2response/msmt_5tt/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/fslgrad.mif dwi2response/msmt_5tt/default.mif.gz dwi2response msmt_5tt BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/grad_wm.txt ../tmp/dwi2response/msmt_5tt/grad_gm.txt ../tmp/dwi2response/msmt_5tt/grad_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/grad.mif -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/grad_wm.txt dwi2response/msmt_5tt/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/grad_gm.txt dwi2response/msmt_5tt/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/grad_csf.txt dwi2response/msmt_5tt/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/grad.mif dwi2response/msmt_5tt/default.mif.gz dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/masked_wm.txt ../tmp/dwi2response/msmt_5tt/masked_gm.txt ../tmp/dwi2response/msmt_5tt/masked_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/masked.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/masked_wm.txt dwi2response/msmt_5tt/masked_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/masked_gm.txt dwi2response/msmt_5tt/masked_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/masked_csf.txt dwi2response/msmt_5tt/masked_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/masked.mif dwi2response/msmt_5tt/masked.mif.gz -dwi2tensor tmp-sub-01_dwi.mif - -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz | tensor2metric - -vector tmp-dirs.mif -force && dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/dirs_wm.txt ../tmp/dwi2response/msmt_5tt/dirs_gm.txt ../tmp/dwi2response/msmt_5tt/dirs_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/dirs.mif -dirs tmp-dirs.mif -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/dirs_wm.txt dwi2response/msmt_5tt/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/dirs_gm.txt dwi2response/msmt_5tt/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/dirs_csf.txt dwi2response/msmt_5tt/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/dirs.mif dwi2response/msmt_5tt/default.mif.gz +dwi2tensor tmp-sub-01_dwi.mif - -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz | tensor2metric - -vector - | dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/dirs_wm.txt ../tmp/dwi2response/msmt_5tt/dirs_gm.txt ../tmp/dwi2response/msmt_5tt/dirs_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/dirs.mif -dirs - -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/dirs_wm.txt dwi2response/msmt_5tt/default_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/dirs_gm.txt dwi2response/msmt_5tt/default_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/dirs_csf.txt dwi2response/msmt_5tt/default_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/dirs.mif dwi2response/msmt_5tt/default.mif.gz dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/tax_wm.txt ../tmp/dwi2response/msmt_5tt/tax_gm.txt ../tmp/dwi2response/msmt_5tt/tax_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/tax.mif -wm_algo tax -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/tax_wm.txt dwi2response/msmt_5tt/tax_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/tax_gm.txt dwi2response/msmt_5tt/tax_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/tax_csf.txt dwi2response/msmt_5tt/tax_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/tax.mif dwi2response/msmt_5tt/tax.mif.gz dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/sfwmfa_wm.txt ../tmp/dwi2response/msmt_5tt/sfwmfa_gm.txt ../tmp/dwi2response/msmt_5tt/sfwmfa_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/sfwmfa.mif -sfwm_fa_threshold 0.7 -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/sfwmfa_wm.txt dwi2response/msmt_5tt/sfwmfa_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/sfwmfa_gm.txt dwi2response/msmt_5tt/sfwmfa_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/sfwmfa_csf.txt dwi2response/msmt_5tt/sfwmfa_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/sfwmfa.mif dwi2response/msmt_5tt/sfwmfa.mif.gz dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/lmax_wm.txt ../tmp/dwi2response/msmt_5tt/lmax_gm.txt ../tmp/dwi2response/msmt_5tt/lmax_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/lmax.mif -lmax 0,2,4,6 -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/lmax_wm.txt dwi2response/msmt_5tt/lmax_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/lmax_gm.txt dwi2response/msmt_5tt/lmax_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/lmax_csf.txt dwi2response/msmt_5tt/lmax_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/lmax.mif dwi2response/msmt_5tt/lmax.mif.gz dwi2response msmt_5tt tmp-sub-01_dwi.mif BIDS/sub-01/anat/sub-01_5TT.nii.gz ../tmp/dwi2response/msmt_5tt/shells_wm.txt ../tmp/dwi2response/msmt_5tt/shells_gm.txt ../tmp/dwi2response/msmt_5tt/shells_csf.txt -voxels ../tmp/dwi2response/msmt_5tt/shells.mif -shells 0,2000 -pvf 0.9 -force && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/shells_wm.txt dwi2response/msmt_5tt/shells_wm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/shells_gm.txt dwi2response/msmt_5tt/shells_gm.txt -abs 1e-2 && testing_diff_matrix ../tmp/dwi2response/msmt_5tt/shells_csf.txt dwi2response/msmt_5tt/shells_csf.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/msmt_5tt/shells.mif dwi2response/msmt_5tt/shells.mif.gz mkdir -p ../tmp/dwi2response/tax && dwi2response tax tmp-sub-01_dwi.mif ../tmp/dwi2response/tax/default.txt -voxels ../tmp/dwi2response/tax/default.mif -force && testing_diff_matrix ../tmp/dwi2response/tax/default.txt dwi2response/tax/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tax/default.mif dwi2response/tax/default.mif.gz +dwi2response tax tmp-sub-01_dwi.mif ../tmp/dwi2response/tax/default.txt -voxels - -force | testing_diff_image - dwi2response/tax/default.mif.gz dwi2response tax BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwi2response/tax/fslgrad.txt -voxels ../tmp/dwi2response/tax/fslgrad.mif -force && testing_diff_matrix ../tmp/dwi2response/tax/fslgrad.txt dwi2response/tax/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tax/fslgrad.mif dwi2response/tax/default.mif.gz dwi2response tax BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b ../tmp/dwi2response/tax/grad.txt -voxels ../tmp/dwi2response/tax/grad.mif -force && testing_diff_matrix ../tmp/dwi2response/tax/grad.txt dwi2response/tax/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tax/grad.mif dwi2response/tax/default.mif.gz dwi2response tax tmp-sub-01_dwi.mif ../tmp/dwi2response/tax/masked.txt -voxels ../tmp/dwi2response/tax/masked.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_matrix ../tmp/dwi2response/tax/masked.txt dwi2response/tax/masked.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tax/masked.mif dwi2response/tax/masked.mif.gz dwi2response tax tmp-sub-01_dwi.mif ../tmp/dwi2response/tax/lmax.txt -voxels ../tmp/dwi2response/tax/lmax.mif -lmax 6 -force && testing_diff_matrix ../tmp/dwi2response/tax/lmax.txt dwi2response/tax/lmax.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tax/lmax.mif dwi2response/tax/lmax.mif.gz dwi2response tax tmp-sub-01_dwi.mif ../tmp/dwi2response/tax/shell.txt -voxels ../tmp/dwi2response/tax/shell.mif -shell 2000 -force && testing_diff_matrix ../tmp/dwi2response/tax/shell.txt dwi2response/tax/shell.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tax/shell.mif dwi2response/tax/shell.mif.gz mkdir -p ../tmp/dwi2response/tournier && dwi2response tournier tmp-sub-01_dwi.mif ../tmp/dwi2response/tournier/default.txt -voxels ../tmp/dwi2response/tournier/default.mif -number 20 -iter_voxels 200 -force && testing_diff_matrix ../tmp/dwi2response/tournier/default.txt dwi2response/tournier/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tournier/default.mif dwi2response/tournier/default.mif.gz +dwi2response tournier tmp-sub-01_dwi.mif ../tmp/dwi2response/tournier/default.txt -voxels - -number 20 -iter_voxels 200 -force | testing_diff_image - dwi2response/tournier/default.mif.gz dwi2response tournier BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwi2response/tournier/fslgrad.txt -voxels ../tmp/dwi2response/tournier/fslgrad.mif -number 20 -iter_voxels 200 -force && testing_diff_matrix ../tmp/dwi2response/tournier/fslgrad.txt dwi2response/tournier/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tournier/fslgrad.mif dwi2response/tournier/default.mif.gz dwi2response tournier BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b ../tmp/dwi2response/tournier/grad.txt -voxels ../tmp/dwi2response/tournier/grad.mif -number 20 -iter_voxels 200 -force && testing_diff_matrix ../tmp/dwi2response/tournier/grad.txt dwi2response/tournier/default.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tournier/grad.mif dwi2response/tournier/default.mif.gz dwi2response tournier tmp-sub-01_dwi.mif ../tmp/dwi2response/tournier/masked.txt -voxels ../tmp/dwi2response/tournier/masked.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -number 20 -iter_voxels 200 -force && testing_diff_matrix ../tmp/dwi2response/tournier/masked.txt dwi2response/tournier/masked.txt -abs 1e-2 && testing_diff_image ../tmp/dwi2response/tournier/masked.mif dwi2response/tournier/masked.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect b/testing/scripts/tests/dwibiascorrect index c75f2f6632..a01debff72 100644 --- a/testing/scripts/tests/dwibiascorrect +++ b/testing/scripts/tests/dwibiascorrect @@ -1,7 +1,10 @@ mkdir -p ../tmp/dwibiascorrect/ants && mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval tmp-sub-01_dwi.mif -export_grad_mrtrix tmp-sub-01_dwi.b -strides 0,0,0,1 && dwibiascorrect ants tmp-sub-01_dwi.mif ../tmp/dwibiascorrect/ants/default_out.mif -bias ../tmp/dwibiascorrect/ants/default_bias.mif -force && testing_diff_header ../tmp/dwibiascorrect/ants/default_out.mif dwibiascorrect/ants/default_out.mif.gz && testing_diff_header ../tmp/dwibiascorrect/ants/default_bias.mif dwibiascorrect/ants/default_bias.mif.gz +mrconvert tmp-sub-01_dwi.mif - | dwibiascorrect ants - - | testing_diff_header - dwibiascorrect/ants/default_out.mif.gz +dwibiascorrect ants tmp-sub-01_dwi.mif ../tmp/dwibiascorrect/ants/default_out.mif -bias - -force | testing_diff_header - dwibiascorrect/ants/default_bias.mif.gz dwibiascorrect ants BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwibiascorrect/ants/fslgrad_out.mif -bias ../tmp/dwibiascorrect/ants/fslgrad_bias.mif -force && testing_diff_header ../tmp/dwibiascorrect/ants/fslgrad_out.mif dwibiascorrect/ants/default_out.mif.gz && testing_diff_header ../tmp/dwibiascorrect/ants/fslgrad_bias.mif dwibiascorrect/ants/default_bias.mif.gz dwibiascorrect ants BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b ../tmp/dwibiascorrect/ants/grad_out.mif -bias ../tmp/dwibiascorrect/ants/grad_bias.mif -force && testing_diff_header ../tmp/dwibiascorrect/ants/grad_out.mif dwibiascorrect/ants/default_out.mif.gz && testing_diff_header ../tmp/dwibiascorrect/ants/grad_bias.mif dwibiascorrect/ants/default_bias.mif.gz dwibiascorrect ants tmp-sub-01_dwi.mif ../tmp/dwibiascorrect/ants/masked_out.mif -bias ../tmp/dwibiascorrect/ants/masked_bias.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_header ../tmp/dwibiascorrect/ants/masked_out.mif dwibiascorrect/ants/masked_out.mif.gz && testing_diff_header ../tmp/dwibiascorrect/ants/masked_bias.mif dwibiascorrect/ants/masked_bias.mif.gz +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | dwibiascorrect ants tmp-sub-01_dwi.mif ../tmp/dwibiascorrect/ants/masked_out.mif -mask - -force && testing_diff_header ../tmp/dwibiascorrect/ants/masked_out.mif dwibiascorrect/ants/masked_out.mif.gz mkdir -p ../tmp/dwibiascorrect/fsl && dwibiascorrect fsl tmp-sub-01_dwi.mif ../tmp/dwibiascorrect/fsl/default_out.mif -bias ../tmp/dwibiascorrect/fsl/default_bias.mif -force && testing_diff_header ../tmp/dwibiascorrect/fsl/default_out.mif dwibiascorrect/fsl/default_out.mif.gz && testing_diff_header ../tmp/dwibiascorrect/fsl/default_bias.mif dwibiascorrect/fsl/default_bias.mif.gz dwibiascorrect fsl BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwibiascorrect/fsl/fslgrad_out.mif -bias ../tmp/dwibiascorrect/fsl/fslgrad_bias.mif -force && testing_diff_header ../tmp/dwibiascorrect/fsl/fslgrad_out.mif dwibiascorrect/fsl/default_out.mif.gz && testing_diff_header ../tmp/dwibiascorrect/fsl/fslgrad_bias.mif dwibiascorrect/fsl/default_bias.mif.gz dwibiascorrect fsl BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b ../tmp/dwibiascorrect/fsl/grad_out.mif -bias ../tmp/dwibiascorrect/fsl/grad_bias.mif -force && testing_diff_header ../tmp/dwibiascorrect/fsl/grad_out.mif dwibiascorrect/fsl/default_out.mif.gz && testing_diff_header ../tmp/dwibiascorrect/fsl/grad_bias.mif dwibiascorrect/fsl/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiasnormmask b/testing/scripts/tests/dwibiasnormmask index d3936e70ee..666f15d02a 100644 --- a/testing/scripts/tests/dwibiasnormmask +++ b/testing/scripts/tests/dwibiasnormmask @@ -1,4 +1,7 @@ mkdir -p ../tmp/dwibiasnormmask && mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval tmp-sub-01_dwi.mif -force && dwibiasnormmask tmp-sub-01_dwi.mif ../tmp/dwibiasnormmask/default_out.mif ../tmp/dwibiasnormmask/default_mask.mif -output_bias ../tmp/dwibiasnormmask/default_bias.mif -output_scale ../tmp/dwibiasnormmask/default_scale.txt -output_tissuesum ../tmp/dwibiasnormmask/default_tissuesum.mif -force && testing_diff_image ../tmp/dwibiasnormmask/default_out.mif dwibiasnormmask/default_out.mif.gz -frac 1e-5 && testing_diff_image ../tmp/dwibiasnormmask/default_mask.mif dwibiasnormmask/default_mask.mif.gz && testing_diff_image ../tmp/dwibiasnormmask/default_bias.mif dwibiasnormmask/default_bias.mif.gz -frac 1e-5 && testing_diff_matrix ../tmp/dwibiasnormmask/default_scale.txt dwibiasnormmask/default_scale.txt -frac 1e-5 && testing_diff_image ../tmp/dwibiasnormmask/default_tissuesum.mif dwibiasnormmask/default_tissuesum.mif.gz -abs 1e-5 +mrconvert tmp-sub-01_dwi.mif - | dwibiasnormmask - - ../tmp/dwibiasnormmask/default_mask.mif -force | testing_diff_image - dwibiasnormmask/default_out.mif.gz -frac 1e-5 +dwibiasnormmask tmp-sub-01_dwi.mif ../tmp/dwibiasnormmask/default_out.mif ../tmp/dwibiasnormmask/default_mask.mif -output_bias - -force | testing_diff_image - dwibiasnormmask/default_bias.mif.gz -frac 1e-5 +dwibiasnormmask tmp-sub-01_dwi.mif ../tmp/dwibiasnormmask/default_out.mif ../tmp/dwibiasnormmask/default_mask.mif -output_tissuesum - -force | testing_diff_image - dwibiasnormmask/default_tissuesum.mif.gz -abs 1e-5 dwibiasnormmask tmp-sub-01_dwi.mif -max_iters 3 -dice 1.0 ../tmp/dwibiasnormmask/3iters_out.mif ../tmp/dwibiasnormmask/3iters_mask.mif -output_bias ../tmp/dwibiasnormmask/3iters_bias.mif -output_scale ../tmp/dwibiasnormmask/3iters_scale.txt -output_tissuesum ../tmp/dwibiasnormmask/3iters_tissuesum.mif -force && testing_diff_image ../tmp/dwibiasnormmask/3iters_out.mif dwibiasnormmask/3iters_out.mif.gz -frac 1e-5 && testing_diff_image ../tmp/dwibiasnormmask/3iters_mask.mif dwibiasnormmask/3iters_mask.mif.gz && testing_diff_image ../tmp/dwibiasnormmask/3iters_bias.mif dwibiasnormmask/3iters_bias.mif.gz -frac 1e-5 && testing_diff_matrix ../tmp/dwibiasnormmask/3iters_scale.txt dwibiasnormmask/3iters_scale.txt -frac 1e-5 && testing_diff_image ../tmp/dwibiasnormmask/3iters_tissuesum.mif dwibiasnormmask/3iters_tissuesum.mif.gz -abs 1e-5 dwibiasnormmask tmp-sub-01_dwi.mif -lmax 6,0,0 ../tmp/dwibiasnormmask/lmax600_out.mif ../tmp/dwibiasnormmask/lmax600_mask.mif -output_bias ../tmp/dwibiasnormmask/lmax600_bias.mif -output_scale ../tmp/dwibiasnormmask/lmax600_scale.txt -output_tissuesum ../tmp/dwibiasnormmask/lmax600_tissuesum.mif -force && testing_diff_image ../tmp/dwibiasnormmask/lmax600_out.mif dwibiasnormmask/lmax600_out.mif.gz -frac 1e-5 && testing_diff_image ../tmp/dwibiasnormmask/lmax600_mask.mif dwibiasnormmask/lmax600_mask.mif.gz && testing_diff_image ../tmp/dwibiasnormmask/lmax600_bias.mif dwibiasnormmask/lmax600_bias.mif.gz -frac 1e-5 && testing_diff_matrix ../tmp/dwibiasnormmask/lmax600_scale.txt dwibiasnormmask/lmax600_scale.txt -frac 1e-5 && testing_diff_image ../tmp/dwibiasnormmask/lmax600_tissuesum.mif dwibiasnormmask/lmax600_tissuesum.mif.gz -abs 1e-5 dwibiasnormmask tmp-sub-01_dwi.mif -reference 1.0 ../tmp/dwibiasnormmask/reference_out.mif ../tmp/dwibiasnormmask/reference_mask.mif -output_scale ../tmp/dwibiasnormmask/scale.txt -force && mrcalc ../tmp/dwibiasnormmask/reference_out.mif 1000.0 -mult - | testing_diff_image - dwibiasnormmask/default_out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwicat b/testing/scripts/tests/dwicat index 4974fb837e..d1a3e68faa 100644 --- a/testing/scripts/tests/dwicat +++ b/testing/scripts/tests/dwicat @@ -1,4 +1,6 @@ mkdir -p ../tmp/dwicat && mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval tmp.mif -force && dwiextract tmp.mif tmp01_b1000.mif -shells 0,1000 -force && dwiextract tmp.mif tmp01_b2000.mif -shells 0,2000 -force && dwiextract tmp.mif tmp01_b3000.mif -shells 0,3000 -force && mrcat tmp01_b1000.mif tmp01_b2000.mif tmp01_b3000.mif -axis 3 tmp02.mif -force && mrcalc tmp01_b2000.mif 0.2 -mult tmp03_b2000.mif -force && mrcalc tmp01_b3000.mif 5.0 -mult tmp03_b3000.mif && mrcat tmp01_b1000.mif tmp03_b2000.mif tmp03_b3000.mif -axis 3 tmp03.mif -force && dwicat tmp01_b1000.mif tmp03_b2000.mif tmp03_b3000.mif ../tmp/dwicat/sharedb0_masked.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_image ../tmp/dwicat/sharedb0_masked.mif tmp02.mif -frac 1e-6 +dwicat $(mrconvert tmp01_b1000.mif -) $(mrconvert tmp03_b2000.mif -) $(mrconvert tmp03_b3000.mif -) - | testing_diff_image - tmp02.mif -frac 1e-6 +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | dwicat tmp01_b1000.mif tmp03_b2000.mif tmp03_b3000.mif ../tmp/dwicat/sharedb0_masked.mif -mask - -force && testing_diff_image ../tmp/dwicat/sharedb0_masked.mif tmp02.mif -frac 1e-6 dwicat tmp01_b1000.mif tmp03_b2000.mif tmp03_b3000.mif ../tmp/dwicat/sharedb0_unmasked.mif -force && testing_diff_image ../tmp/dwicat/sharedb0_unmasked.mif tmp02.mif -frac 1e-6 dwiextract tmp.mif tmp11_b0.mif -shell 0 -force && dwiextract tmp.mif tmp11_b1000.mif -shell 1000 -force && dwiextract tmp.mif tmp11_b2000.mif -shell 2000 -force && dwiextract tmp.mif tmp11_b3000.mif -shell 3000 -force && mrconvert tmp11_b0.mif -coord 3 0,1 - | mrcat - tmp11_b1000.mif tmp12_b1000.mif -axis 3 -force && mrconvert tmp11_b0.mif -coord 3 2,3 - | mrcat - tmp11_b2000.mif tmp12_b2000.mif -axis 3 -force && mrconvert tmp11_b0.mif -coord 3 4,5 - | mrcat - tmp11_b3000.mif tmp12_b3000.mif -axis 3 -force && mrcalc tmp12_b2000.mif 0.2 -mult tmp13_b2000.mif -force && mrcalc tmp12_b3000.mif 5.0 -mult tmp13_b3000.mif -force && mrcat tmp12_b1000.mif tmp12_b2000.mif tmp12_b3000.mif tmp14.mif -axis 3 -force && dwicat tmp12_b1000.mif tmp13_b2000.mif tmp13_b3000.mif ../tmp/dwicat/ownb0_masked.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -force && testing_diff_image ../tmp/dwicat/ownb0_masked.mif tmp14.mif -frac 0.01 dwicat tmp12_b1000.mif tmp13_b2000.mif tmp13_b3000.mif ../tmp/dwicat/ownb0_unmasked.mif -force && testing_diff_image ../tmp/dwicat/ownb0_unmasked.mif tmp14.mif -frac 0.02 diff --git a/testing/scripts/tests/dwifslpreproc b/testing/scripts/tests/dwifslpreproc index fa3a742d3c..339a1019aa 100644 --- a/testing/scripts/tests/dwifslpreproc +++ b/testing/scripts/tests/dwifslpreproc @@ -1,7 +1,9 @@ mkdir -p ../tmp/dwifslpreproc && mrconvert BIDS/sub-04/dwi/sub-04_dwi.nii.gz -fslgrad BIDS/sub-04/dwi/sub-04_dwi.bvec BIDS/sub-04/dwi/sub-04_dwi.bval -json_import BIDS/sub-04/dwi/sub-04_dwi.json tmp-sub-04_dwi.mif -export_grad_mrtrix tmp-sub-04_dwi.b -strides 0,0,0,1 && dwifslpreproc tmp-sub-04_dwi.mif ../tmp/dwifslpreproc/rpenone_default.mif -pe_dir ap -readout_time 0.1 -rpe_none -force && testing_diff_header ../tmp/dwifslpreproc/rpenone_default.mif dwifslpreproc/rpenone_default.mif.gz +mrconvert tmp-sub-04_dwi.mif - | dwifslpreproc - - -pe_dir ap -readout_time 0.1 -rpe_none | testing_diff_header - dwifslpreproc/rpenone_default.mif.gz dwifslpreproc BIDS/sub-04/dwi/sub-04_dwi.nii.gz -fslgrad BIDS/sub-04/dwi/sub-04_dwi.bvec BIDS/sub-04/dwi/sub-04_dwi.bval ../tmp/dwifslpreproc/fslgrad.mif -export_grad_fsl ../tmp/dwifslpreproc/fslgrad.bvec ../tmp/dwifslpreproc/fslgrad.bval -pe_dir ap -readout_time 0.1 -rpe_none -force && testing_diff_header ../tmp/dwifslpreproc/fslgrad.mif dwifslpreproc/rpenone_default.mif.gz && testing_diff_matrix ../tmp/dwifslpreproc/fslgrad.bvec dwifslpreproc/rpenone_default.bvec -abs 1e-2 && testing_diff_matrix ../tmp/dwifslpreproc/fslgrad.bval dwifslpreproc/rpenone_default.bval dwifslpreproc BIDS/sub-04/dwi/sub-04_dwi.nii.gz -grad tmp-sub-04_dwi.b ../tmp/dwifslpreproc/grad.mif -export_grad_mrtrix ../tmp/dwifslpreproc/grad.b -pe_dir ap -readout_time 0.1 -rpe_none -force && testing_diff_header ../tmp/dwifslpreproc/grad.mif dwifslpreproc/rpenone_default.mif.gz && testing_diff_matrix ../tmp/dwifslpreproc/grad.b dwifslpreproc/rpenone_default.b -abs 1e-2 mrconvert BIDS/sub-04/fmap/sub-04_dir-1_epi.nii.gz -json_import BIDS/sub-04/fmap/sub-04_dir-1_epi.json tmp-sub-04_dir-1_epi.mif -force && mrconvert BIDS/sub-04/fmap/sub-04_dir-2_epi.nii.gz -json_import BIDS/sub-04/fmap/sub-04_dir-2_epi.json tmp-sub-04_dir-2_epi.mif -force && mrcat tmp-sub-04_dir-1_epi.mif tmp-sub-04_dir-2_epi.mif tmp-sub-04_dir-all_epi.mif -axis 3 -force && dwifslpreproc tmp-sub-04_dwi.mif ../tmp/dwifslpreproc/rpepair_default.mif -pe_dir ap -readout_time 0.1 -rpe_pair -se_epi tmp-sub-04_dir-all_epi.mif -force && testing_diff_header ../tmp/dwifslpreproc/rpepair_default.mif dwifslpreproc/rpepair_default.mif.gz +mrconvert tmp-sub-04_dir-all_epi.mif - | dwifslpreproc tmp-sub-04_dwi.mif ../tmp/dwifslpreproc/rpepair_default.mif -pe_dir ap -readout_time 0.1 -rpe_pair -se_epi - -force && testing_diff_header ../tmp/dwifslpreproc/rpepair_default.mif dwifslpreproc/rpepair_default.mif.gz dwifslpreproc tmp-sub-04_dwi.mif ../tmp/dwifslpreproc/rpepair_align.mif -pe_dir ap -readout_time 0.1 -rpe_pair -se_epi tmp-sub-04_dir-all_epi.mif -align_seepi -force && testing_diff_header ../tmp/dwifslpreproc/rpepair_align.mif dwifslpreproc/rpepair_align.mif.gz dwifslpreproc tmp-sub-04_dwi.mif ../tmp/dwifslpreproc/eddyqc_text.mif -pe_dir ap -readout_time 0.1 -rpe_pair -se_epi tmp-sub-04_dir-all_epi.mif -eddyqc_text ../tmp/dwifslpreproc/eddyqc_text/ -force dwifslpreproc tmp-sub-04_dwi.mif ../tmp/dwifslpreproc/eddyqc_all.mif -pe_dir ap -readout_time 0.1 -rpe_pair -se_epi tmp-sub-04_dir-all_epi.mif -eddyqc_all ../tmp/dwifslpreproc/eddyqc_all/ -force diff --git a/testing/scripts/tests/dwigradcheck b/testing/scripts/tests/dwigradcheck index 80431d222d..71a38b2596 100644 --- a/testing/scripts/tests/dwigradcheck +++ b/testing/scripts/tests/dwigradcheck @@ -1,4 +1,7 @@ mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval tmp-sub-01_dwi.mif -export_grad_mrtrix tmp-sub-01_dwi.b -strides 0,0,0,1 && dwigradcheck tmp-sub-01_dwi.mif +mrconvert tmp-sub-01_dwi.mif - | dwigradcheck - mkdir -p ../tmp/dwigradcheck && dwigradcheck BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval -export_grad_fsl ../tmp/dwigradcheck/fsl.bvec ../tmp/dwigradcheck/fsl.bval -force && testing_diff_matrix ../tmp/dwigradcheck/fsl.bvec dwigradcheck/fsl.bvec -abs 1e-3 dwigradcheck BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp-sub-01_dwi.b -export_grad_mrtrix ../tmp/dwigradcheck/grad.b -force && testing_diff_matrix ../tmp/dwigradcheck/grad.b dwigradcheck/grad.b -abs 1e-3 dwigradcheck tmp-sub-01_dwi.mif -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | dwigradcheck tmp-sub-01_dwi.mif -mask - +dwigradcheck $(mrconvert tmp-sub-01_dwi.mif -) -mask $(mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -) \ No newline at end of file diff --git a/testing/scripts/tests/dwinormalise b/testing/scripts/tests/dwinormalise index 1b66ebfd0e..25385e4331 100644 --- a/testing/scripts/tests/dwinormalise +++ b/testing/scripts/tests/dwinormalise @@ -1,8 +1,14 @@ mkdir -p ../tmp/dwinormalise/group && mkdir -p tmp-dwi && mkdir -p tmp-mask && mrconvert BIDS/sub-02/dwi/sub-02_dwi.nii.gz -fslgrad BIDS/sub-02/dwi/sub-02_dwi.bvec BIDS/sub-02/dwi/sub-02_dwi.bval tmp-dwi/sub-02.mif -force && mrconvert BIDS/sub-02/dwi/sub-02_brainmask.nii.gz tmp-mask/sub-02.mif -force && mrconvert BIDS/sub-03/dwi/sub-03_dwi.nii.gz -fslgrad BIDS/sub-03/dwi/sub-03_dwi.bvec BIDS/sub-03/dwi/sub-03_dwi.bval tmp-dwi/sub-03.mif -force && mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz tmp-mask/sub-03.mif -force && dwinormalise group tmp-dwi/ tmp-mask/ ../tmp/dwinormalise/group/ ../tmp/dwinormalise/group/fa.mif ../tmp/dwinormalise/group/mask.mif -force && testing_diff_image ../tmp/dwinormalise/group/sub-02.mif dwinormalise/group/sub-02.mif.gz -frac 1e-2 && testing_diff_image ../tmp/dwinormalise/group/sub-03.mif dwinormalise/group/sub-03.mif.gz -frac 1e-2 && testing_diff_image ../tmp/dwinormalise/group/fa.mif dwinormalise/group/fa.mif.gz -abs 1e-3 && testing_diff_image $(mrfilter ../tmp/dwinormalise/group/mask.mif smooth -) $(mrfilter dwinormalise/group/mask.mif.gz smooth -) -abs 0.3 +dwinormalise group tmp-dwi/ tmp-mask/ ../tmp/dwinormalise/group/ - ../tmp/dwinormalise/group/mask.mif -force | testing_diff_image - dwinormalise/group/fa.mif.gz -abs 1e-3 +dwinormalise group tmp-dwi/ tmp-mask/ ../tmp/dwinormalise/group/ ../tmp/dwinormalise/group/fa.mif - -force | testing_diff_image $(mrfilter - smooth -) $(mrfilter dwinormalise/group/mask.mif.gz smooth -) -abs 0.3 mkdir ../tmp/dwinormalise/manual && dwinormalise manual BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval BIDS/sub-01/dwi/sub-01_brainmask.nii.gz ../tmp/dwinormalise/manual/out.mif -force && testing_diff_image ../tmp/dwinormalise/manual/out.mif dwinormalise/manual/out.mif.gz -frac 1e-5 +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval - | dwinormalise manual - BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | testing_diff_image - dwinormalise/manual/out.mif.gz -frac 1e-5 +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | dwinormalise manual BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval - ../tmp/dwinormalise/manual/out.mif -force && testing_diff_image ../tmp/dwinormalise/manual/out.mif dwinormalise/manual/out.mif.gz -frac 1e-5 dwinormalise manual BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval BIDS/sub-01/dwi/sub-01_brainmask.nii.gz ../tmp/dwinormalise/manual/percentile.mif -percentile 40 -force && testing_diff_image ../tmp/dwinormalise/manual/percentile.mif dwinormalise/manual/percentile.mif.gz -frac 1e-5 mkdir ../tmp/dwinormalise/mtnorm && dwinormalise mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwinormalise/mtnorm/default_out.mif -scale ../tmp/dwinormalise/mtnorm/default_scale.txt -force && testing_diff_image ../tmp/dwinormalise/mtnorm/default_out.mif dwinormalise/mtnorm/default_out.mif.gz -frac 1e-5 && testing_diff_matrix ../tmp/dwinormalise/mtnorm/default_scale.txt dwinormalise/mtnorm/default_scale.txt -abs 1e-5 +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval - | dwinormalise mtnorm - - | testing_diff_image - dwinormalise/mtnorm/default_out.mif.gz -frac 1e-5 dwinormalise mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwinormalise/mtnorm/masked_out.mif -scale ../tmp/dwinormalise/mtnorm/masked_scale.txt -force && testing_diff_image ../tmp/dwinormalise/mtnorm/masked_out.mif dwinormalise/mtnorm/masked_out.mif.gz -frac 1e-5 && testing_diff_matrix ../tmp/dwinormalise/mtnorm/masked_scale.txt dwinormalise/mtnorm/masked_scale.txt -abs 1e-5 +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | dwinormalise mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz -mask - -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwinormalise/mtnorm/masked_out.mif -force | testing_diff_image ../tmp/dwinormalise/mtnorm/masked_out.mif dwinormalise/mtnorm/masked_out.mif.gz -frac 1e-5 dwinormalise mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz -lmax 6,0,0 -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwinormalise/mtnorm/lmax600_out.mif -scale ../tmp/dwinormalise/mtnorm/lmax600_scale.txt -force && testing_diff_image ../tmp/dwinormalise/mtnorm/lmax600_out.mif dwinormalise/mtnorm/lmax600_out.mif.gz -frac 1e-5 && testing_diff_matrix ../tmp/dwinormalise/mtnorm/lmax600_scale.txt dwinormalise/mtnorm/lmax600_scale.txt -abs 1e-5 mrcalc BIDS/sub-01/dwi/sub-01_dwi.nii.gz 1000.0 -div tmp-sub-01_dwi_scaled.mif -force && dwinormalise mtnorm tmp-sub-01_dwi_scaled.mif -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwinormalise/mtnorm/scaled_out.mif -scale ../tmp/dwinormalise/mtnorm/scaled_scale.txt -force && testing_diff_image ../tmp/dwinormalise/mtnorm/scaled_out.mif dwinormalise/mtnorm/default_out.mif.gz -frac 1e-5 dwinormalise mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz -reference 1.0 -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ../tmp/dwinormalise/mtnorm/reference_out.mif -scale ../tmp/dwinormalise/mtnorm/reference_scale.txt -force && mrcalc ../tmp/dwinormalise/mtnorm/reference_out.mif 1000.0 -mult - | testing_diff_image - dwinormalise/mtnorm/default_out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwishellmath b/testing/scripts/tests/dwishellmath index a3421e5777..41e47fca2e 100644 --- a/testing/scripts/tests/dwishellmath +++ b/testing/scripts/tests/dwishellmath @@ -1,4 +1,5 @@ mkdir -p ../tmp/dwishellmath && mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval tmp1.mif -export_grad_mrtrix tmp1.b -force && dwiextract tmp1.mif tmp1_b0.mif -shell 0 -force && dwiextract tmp1.mif tmp1_b1000.mif -shell 1000 -force && dwiextract tmp1.mif tmp1_b2000.mif -shell 2000 -force && dwiextract tmp1.mif tmp1_b3000.mif -shell 3000 -force && mrmath tmp1_b0.mif mean tmp2_b0.mif -axis 3 -force && mrmath tmp1_b1000.mif mean tmp2_b1000.mif -axis 3 -force && mrmath tmp1_b2000.mif mean tmp2_b2000.mif -axis 3 -force && mrmath tmp1_b3000.mif mean tmp2_b3000.mif -axis 3 -force && mrcat tmp2_b0.mif tmp2_b1000.mif tmp2_b2000.mif tmp2_b3000.mif -axis 3 tmp2.mif -force && dwishellmath tmp1.mif mean ../tmp/dwishellmath/default.mif -force && testing_diff_image ../tmp/dwishellmath/default.mif tmp2.mif +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval - | dwishellmath - mean - | testing_diff_image - tmp2.mif dwishellmath BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval mean ../tmp/dwishellmath/fslgrad.mif -force && testing_diff_image ../tmp/dwishellmath/fslgrad.mif tmp2.mif dwishellmath BIDS/sub-01/dwi/sub-01_dwi.nii.gz -grad tmp1.b mean ../tmp/dwishellmath/grad.mif -force && testing_diff_image ../tmp/dwishellmath/grad.mif tmp2.mif dwishellmath tmp1_b3000.mif mean ../tmp/dwishellmath/onebvalue.mif -force diff --git a/testing/scripts/tests/labelsgmfix b/testing/scripts/tests/labelsgmfix index 93aa7c45c7..2da7d232e1 100644 --- a/testing/scripts/tests/labelsgmfix +++ b/testing/scripts/tests/labelsgmfix @@ -1,4 +1,6 @@ mkdir -p ../tmp/labelsgmfix && labelsgmfix BIDS/sub-01/anat/aparc+aseg.mgz BIDS/sub-01/anat/sub-01_T1w.nii.gz labelsgmfix/FreeSurferColorLUT.txt ../tmp/labelsgmfix/freesurfer.mif -force && testing_diff_header ../tmp/labelsgmfix/freesurfer.mif labelsgmfix/freesurfer.mif.gz +mrconvert BIDS/sub-01/anat/aparc+aseg.mgz - | labelsgmfix - BIDS/sub-01/anat/sub-01_T1w.nii.gz labelsgmfix/FreeSurferColorLUT.txt - | testing_diff_header - labelsgmfix/freesurfer.mif.gz +labelsgmfix $(mrconvert BIDS/sub-01/anat/aparc+aseg.mgz -) $(mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz -) labelsgmfix/FreeSurferColorLUT.txt ../tmp/labelsgmfix/freesurfer.mif -force && testing_diff_header ../tmp/labelsgmfix/freesurfer.mif labelsgmfix/freesurfer.mif.gz labelsgmfix BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz BIDS/sub-01/anat/sub-01_T1w.nii.gz BIDS/parc-desikan_lookup.txt ../tmp/labelsgmfix/default.mif -force && testing_diff_header ../tmp/labelsgmfix/default.mif labelsgmfix/default.mif.gz mrcalc BIDS/sub-01/anat/sub-01_T1w.nii.gz BIDS/sub-01/anat/sub-01_brainmask.nii.gz -mult tmp.mif -force && labelsgmfix BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz tmp.mif BIDS/parc-desikan_lookup.txt ../tmp/labelsgmfix/premasked.mif -premasked -force && testing_diff_header ../tmp/labelsgmfix/premasked.mif labelsgmfix/premasked.mif.gz labelsgmfix BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz BIDS/sub-01/anat/sub-01_T1w.nii.gz BIDS/parc-desikan_lookup.txt ../tmp/labelsgmfix/sgm_amyg_hipp.mif -sgm_amyg_hipp -force && testing_diff_header ../tmp/labelsgmfix/sgm_amyg_hipp.mif labelsgmfix/sgm_amyg_hipp.mif.gz diff --git a/testing/scripts/tests/population_template b/testing/scripts/tests/population_template index 58b9cb02d5..d1b0ed1618 100644 --- a/testing/scripts/tests/population_template +++ b/testing/scripts/tests/population_template @@ -1,5 +1,7 @@ mkdir -p ../tmp/population_template && mkdir -p tmp-mask && mkdir -p tmp-fa && mkdir -p tmp-fod && mrconvert BIDS/sub-02/dwi/sub-02_brainmask.nii.gz tmp-mask/sub-02.mif -force && mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz tmp-mask/sub-03.mif -force && dwi2tensor BIDS/sub-02/dwi/sub-02_dwi.nii.gz -fslgrad BIDS/sub-02/dwi/sub-02_dwi.bvec BIDS/sub-02/dwi/sub-02_dwi.bval -mask BIDS/sub-02/dwi/sub-02_brainmask.nii.gz - | tensor2metric - -fa tmp-fa/sub-02.mif -force && dwi2tensor BIDS/sub-03/dwi/sub-03_dwi.nii.gz -fslgrad BIDS/sub-03/dwi/sub-03_dwi.bvec BIDS/sub-03/dwi/sub-03_dwi.bval -mask BIDS/sub-03/dwi/sub-03_brainmask.nii.gz - | tensor2metric - -fa tmp-fa/sub-03.mif -force && population_template tmp-fa ../tmp/population_template/fa_default_template.mif -warp_dir ../tmp/population_template/fa_default_warpdir/ -transformed_dir ../tmp/population_template/fa_default_transformeddir/ -linear_transformations_dir ../tmp/population_template/fa_default_lineartransformsdir/ -force && testing_diff_image ../tmp/population_template/fa_default_template.mif population_template/fa_default_template.mif.gz -abs 0.01 +population_template tmp-fa/ - | testing_diff_image - population_template/fa_default_template.mif.gz -abs 0.01 population_template tmp-fa/ ../tmp/population_template/fa_masked_template.mif -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_masked_mask.mif -force && testing_diff_image ../tmp/population_template/fa_masked_template.mif population_template/fa_masked_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_masked_mask.mif smooth -) $(mrfilter population_template/fa_masked_mask.mif.gz smooth -) -abs 0.3 +population_template tmp-fa/ ../tmp/population_template/fa_masked_template.mif -mask_dir tmp-mask/ -template_mask - | testing_diff_image $(mrfilter - smooth -) $(mrfilter population_template/fa_masked_mask.mif.gz smooth -) -abs 0.3 population_template tmp-fa/ ../tmp/population_template/fa_rigid_template.mif -type rigid -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_rigid_mask.mif -force && testing_diff_image ../tmp/population_template/fa_rigid_template.mif population_template/fa_rigid_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_rigid_mask.mif smooth -) $(mrfilter population_template/fa_rigid_mask.mif.gz smooth -) -abs 0.3 population_template tmp-fa/ ../tmp/population_template/fa_affine_template.mif -type affine -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_affine_mask.mif -force && testing_diff_image ../tmp/population_template/fa_affine_template.mif population_template/fa_affine_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_affine_mask.mif smooth -) $(mrfilter population_template/fa_affine_mask.mif.gz smooth -) -abs 0.3 population_template tmp-fa/ ../tmp/population_template/fa_nonlinear_template.mif -type nonlinear -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_nonlinear_mask.mif -force && testing_diff_image ../tmp/population_template/fa_nonlinear_template.mif population_template/fa_nonlinear_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_nonlinear_mask.mif smooth -) $(mrfilter population_template/fa_nonlinear_mask.mif.gz smooth -) -abs 0.3 From ff86c44de38b6ac19fe0c892fc91d0c5f143d8ae Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 19 Feb 2024 12:47:07 +1100 Subject: [PATCH 36/75] Python API: Fix __print_full_usage__ for algorithm wrappers --- lib/mrtrix3/app.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/mrtrix3/app.py b/lib/mrtrix3/app.py index f6580cec5c..29cc181154 100644 --- a/lib/mrtrix3/app.py +++ b/lib/mrtrix3/app.py @@ -1212,9 +1212,12 @@ def arg2str(arg): def allow_multiple(nargs): return '1' if nargs in ('*', '+') else '0' - for arg in self._positionals._group_actions: - sys.stdout.write(f'ARGUMENT {arg.dest} 0 {allow_multiple(arg.nargs)} {arg2str(arg)}\n') - sys.stdout.write(f'{arg.help}\n') + if self._subparsers: + sys.stdout.write(f'ARGUMENT algorithm 0 0 CHOICE {" ".join(self._subparsers._group_actions[0].choices)}\n') + else: + for arg in self._positionals._group_actions: + sys.stdout.write(f'ARGUMENT {arg.dest} 0 {allow_multiple(arg.nargs)} {arg2str(arg)}\n') + sys.stdout.write(f'{arg.help}\n') def print_group_options(group): for option in group._group_actions: From 24f1c322401ebed4b5a83ecc2aa74897ca000b5f Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 19 Feb 2024 15:26:52 +1100 Subject: [PATCH 37/75] Fix Python issues identified by latest tests --- bin/dwibiasnormmask | 4 ++++ bin/dwigradcheck | 4 ++-- testing/scripts/tests/5ttgen | 4 ++-- testing/scripts/tests/dwi2mask | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bin/dwibiasnormmask b/bin/dwibiasnormmask index 8f1ea152e3..3559c2aecf 100755 --- a/bin/dwibiasnormmask +++ b/bin/dwibiasnormmask @@ -425,16 +425,19 @@ def execute(): #pylint: disable=unused-variable run.command(['mrconvert', dwi_image, app.ARGS.output_dwi], mrconvert_keyval=app.ARGS.input, + preserve_pipes=True, force=app.FORCE_OVERWRITE) if app.ARGS.output_bias: run.command(['mrconvert', bias_field_image, app.ARGS.output_bias], mrconvert_keyval=app.ARGS.input, + preserve_pipes=True, force=app.FORCE_OVERWRITE) if app.ARGS.output_mask: run.command(['mrconvert', dwi_mask_image, app.ARGS.output_mask], mrconvert_keyval=app.ARGS.input, + preserve_pipes=True, force=app.FORCE_OVERWRITE) if app.ARGS.output_scale: @@ -445,6 +448,7 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.output_tissuesum: run.command(['mrconvert', tissue_sum_image, app.ARGS.output_tissuesum], mrconvert_keyval=app.ARGS.input, + preserve_pipes=True, force=app.FORCE_OVERWRITE) diff --git a/bin/dwigradcheck b/bin/dwigradcheck index d6efe5226f..5b39ea2288 100755 --- a/bin/dwigradcheck +++ b/bin/dwigradcheck @@ -212,9 +212,9 @@ def execute(): #pylint: disable=unused-variable perm = ''.join(map(str, best[2])) suffix = f'_flip{best[1]}_perm{perm}_{best[3]}' if best[3] == 'scanner': - grad_import_option = ['-grad', 'grad{suffix}.b'] + grad_import_option = ['-grad', f'grad{suffix}.b'] elif best[3] == 'image': - grad_import_option = ['-fslgrad', 'bvecs{suffix}', 'bvals'] + grad_import_option = ['-fslgrad', f'bvecs{suffix}', 'bvals'] else: assert False run.command(['mrinfo', 'data.mif'] diff --git a/testing/scripts/tests/5ttgen b/testing/scripts/tests/5ttgen index 9c62b6564c..69eb1e266c 100644 --- a/testing/scripts/tests/5ttgen +++ b/testing/scripts/tests/5ttgen @@ -1,5 +1,5 @@ mkdir -p ../tmp/5ttgen/freesurfer && 5ttgen freesurfer BIDS/sub-01/anat/aparc+aseg.mgz ../tmp/5ttgen/freesurfer/default.mif -force && testing_diff_image ../tmp/5ttgen/freesurfer/default.mif 5ttgen/freesurfer/default.mif.gz -mrconvert BIDS/sub-01/anat/aparc+aseg.mgz - | 5ttgen freesurfer - - && testing_diff_image - 5ttgen/freesurfer/default.mif.gz +mrconvert BIDS/sub-01/anat/aparc+aseg.mgz - | 5ttgen freesurfer - - | testing_diff_image - 5ttgen/freesurfer/default.mif.gz 5ttgen freesurfer BIDS/sub-01/anat/aparc+aseg.mgz ../tmp/5ttgen/freesurfer/nocrop.mif -nocrop -force && testing_diff_image ../tmp/5ttgen/freesurfer/nocrop.mif 5ttgen/freesurfer/nocrop.mif.gz 5ttgen freesurfer BIDS/sub-01/anat/aparc+aseg.mgz ../tmp/5ttgen/freesurfer/sgm_amyg_hipp.mif -sgm_amyg_hipp -force && testing_diff_image ../tmp/5ttgen/freesurfer/sgm_amyg_hipp.mif 5ttgen/freesurfer/sgm_amyg_hipp.mif.gz mkdir -p ../tmp/5ttgen/fsl && 5ttgen fsl BIDS/sub-01/anat/sub-01_T1w.nii.gz ../tmp/5ttgen/fsl/default.mif -force # && testing_diff_header ../tmp/5ttgen/fsl/default.mif 5ttgen/fsl/default.mif.gz @@ -9,7 +9,7 @@ mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz - | 5ttgen fsl - - | mrconvert - .. 5ttgen fsl BIDS/sub-01/anat/sub-01_T1w.nii.gz ../tmp/5ttgen/fsl/masked.mif -mask BIDS/sub-01/anat/sub-01_brainmask.nii.gz -force && testing_diff_header ../tmp/5ttgen/fsl/masked.mif 5ttgen/fsl/masked.mif.gz mrcalc BIDS/sub-01/anat/sub-01_T1w.nii.gz BIDS/sub-01/anat/sub-01_brainmask.nii.gz -mult tmp1.mif -force && 5ttgen fsl tmp1.mif ../tmp/5ttgen/fsl/premasked.mif -premasked -force && testing_diff_header ../tmp/5ttgen/fsl/premasked.mif 5ttgen/fsl/masked.mif.gz mkdir -p ../tmp/5ttgen/hsvs && 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/default.mif -force && testing_diff_header ../tmp/5ttgen/hsvs/default.mif 5ttgen/hsvs/default.mif.gz -5ttgen hsvs freesurfer/sub-01 - && testing_diff_header - 5ttgen/hsvs/default.mif.gz +5ttgen hsvs freesurfer/sub-01 - | testing_diff_header - 5ttgen/hsvs/default.mif.gz 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/white_stem.mif -white_stem -force && testing_diff_header ../tmp/5ttgen/hsvs/white_stem.mif 5ttgen/hsvs/white_stem.mif.gz 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/modules.mif -hippocampi subfields -thalami nuclei -force && testing_diff_header ../tmp/5ttgen/hsvs/modules.mif 5ttgen/hsvs/modules.mif.gz 5ttgen hsvs freesurfer/sub-01 ../tmp/5ttgen/hsvs/first.mif -hippocampi first -thalami first -force && testing_diff_header ../tmp/5ttgen/hsvs/first.mif 5ttgen/hsvs/first.mif.gz diff --git a/testing/scripts/tests/dwi2mask b/testing/scripts/tests/dwi2mask index d98f3dc403..0d702f7e84 100644 --- a/testing/scripts/tests/dwi2mask +++ b/testing/scripts/tests/dwi2mask @@ -1,7 +1,7 @@ mkdir -p ../tmp/dwi2mask && mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval tmp-sub-01_dwi.mif -force && dwi2mask 3dautomask tmp-sub-01_dwi.mif ../tmp/dwi2mask/3dautomask_default.mif dwi2mask 3dautomask tmp-sub-01_dwi.mif ../tmp/dwi2mask/3dautomask_options.mif -clfrac 0.5 -nograd -peels 1 -nbhrs 17 -eclip -SI 130 -dilate 0 -erode 0 -NN1 -NN2 -NN3 dwi2mask ants tmp-sub-01_dwi.mif ../tmp/dwi2mask/ants_default.mif -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -force -dwi2mask ants tmp-sub-01_dwi.mif ../tmp/dwi2mask/ants_config.mif -config Dwi2maskTemplateImage dwi2mask/template_image.mif.gz -config Dwi2maskTemplateMask dwi2mask/template_mask.mif.gz -force +dwi2mask ants tmp-sub-01_dwi.mif ../tmp/dwi2mask/ants_config.mif -config Dwi2maskTemplateImage $(pwd)/dwi2mask/template_image.mif.gz -config Dwi2maskTemplateMask $(pwd)/dwi2mask/template_mask.mif.gz -force dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsfull_default.mif -software antsfull -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -force dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsquick_default.mif -software antsquick -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz -force mrconvert dwi2mask/template_image.mif.gz - | dwi2mask b02template tmp-sub-01_dwi.mif ../tmp/dwi2mask/template_antsquick_default.mif -software antsquick -template - dwi2mask/template_mask.mif.gz -force @@ -26,7 +26,7 @@ dwi2mask legacy tmp-sub-01_dwi.mif ../tmp/dwi2mask/legacy.mif -force && testing_ mrconvert tmp-sub-01_dwi.mif - | dwi2mask legacy - - | testing_diff_image - dwi2mask/legacy.mif.gz dwi2mask mean tmp-sub-01_dwi.mif ../tmp/dwi2mask/mean.mif -force && testing_diff_image ../tmp/dwi2mask/mean.mif dwi2mask/mean.mif.gz dwi2mask mtnorm tmp-sub-01_dwi.mif ../tmp/dwi2mask/mtnorm_default_mask.mif -tissuesum ../tmp/dwi2mask/mtnorm_default_tissuesum.mif -force && testing_diff_image ../tmp/dwi2mask/mtnorm_default_mask.mif dwi2mask/mtnorm_default_mask.mif.gz && testing_diff_image ../tmp/dwi2mask/mtnorm_default_tissuesum.mif dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 -dwi2mask mtnorm tmp-sub-01_dwi.mif ../tmp/dwi2mask/mtnorm_default_mask.mif -tissuesum - | testing_diff_image - dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 +dwi2mask mtnorm tmp-sub-01_dwi.mif ../tmp/dwi2mask/mtnorm_default_mask.mif -tissuesum - -force | testing_diff_image - dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 dwi2mask mtnorm tmp-sub-01_dwi.mif -init_mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz ../tmp/dwi2mask/mtnorm_initmask_mask.mif -tissuesum ../tmp/dwi2mask/mtnorm_initmask_tissuesum.mif -force && testing_diff_image ../tmp/dwi2mask/mtnorm_initmask_mask.mif dwi2mask/mtnorm_initmask_mask.mif.gz && testing_diff_image ../tmp/dwi2mask/mtnorm_initmask_tissuesum.mif dwi2mask/mtnorm_initmask_tissuesum.mif.gz -abs 1e-5 dwi2mask mtnorm tmp-sub-01_dwi.mif -lmax 6,0,0 ../tmp/dwi2mask/mtnorm_lmax600_mask.mif -tissuesum ../tmp/dwi2mask/mtnorm_lmax600_tissuesum.mif -force && testing_diff_image ../tmp/dwi2mask/mtnorm_lmax600_mask.mif dwi2mask/mtnorm_lmax600_mask.mif.gz && testing_diff_image ../tmp/dwi2mask/mtnorm_lmax600_tissuesum.mif dwi2mask/mtnorm_lmax600_tissuesum.mif.gz -abs 1e-5 dwi2mask synthstrip tmp-sub-01_dwi.mif ../tmp/dwi2mask/synthstrip_default.mif -force From a483d00d99ef31933751e662672b5548fccadc6f Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 28 Feb 2024 17:00:28 +1100 Subject: [PATCH 38/75] Python: Code formatting & documentation regeneration - Some limited modification of code structure to be more faithful to C++ changes in #2818. - Resolution of documentation changes occurring in #2678, particularly in its merge to dev following cmake adoption (#2689). --- docs/reference/commands/5ttgen.rst | 10 +- docs/reference/commands/dwi2mask.rst | 24 +- docs/reference/commands/dwi2response.rst | 19 +- docs/reference/commands/dwibiascorrect.rst | 4 +- docs/reference/commands/dwibiasnormmask.rst | 16 +- docs/reference/commands/dwifslpreproc.rst | 13 +- docs/reference/commands/dwishellmath.rst | 2 +- docs/reference/commands/for_each.rst | 4 +- .../commands/population_template.rst | 24 +- docs/reference/commands/responsemean.rst | 21 +- python/bin/5ttgen | 21 +- python/bin/dwi2mask | 12 +- python/bin/dwi2response | 19 +- python/bin/dwibiasnormmask | 139 +++++---- python/bin/dwicat | 17 +- python/bin/dwifslpreproc | 248 ++++++++++------- python/bin/dwigradcheck | 8 +- python/bin/dwinormalise | 13 +- python/bin/dwishellmath | 14 +- python/bin/for_each | 112 ++++---- python/bin/labelsgmfix | 8 +- python/bin/mask2glass | 29 +- python/bin/mrtrix_cleanup | 30 +- python/bin/population_template | 263 ++++++++++-------- python/bin/responsemean | 24 +- python/lib/mrtrix3/dwi2mask/b02template.py | 9 +- python/lib/mrtrix3/dwi2mask/consensus.py | 6 +- python/lib/mrtrix3/dwi2mask/mtnorm.py | 7 +- python/lib/mrtrix3/dwi2mask/trace.py | 4 +- python/lib/mrtrix3/dwi2response/dhollander.py | 11 +- python/lib/mrtrix3/dwi2response/tax.py | 3 +- python/lib/mrtrix3/dwi2response/tournier.py | 3 +- python/lib/mrtrix3/dwibiascorrect/ants.py | 3 +- python/lib/mrtrix3/dwibiascorrect/mtnorm.py | 4 +- python/lib/mrtrix3/dwinormalise/mtnorm.py | 4 +- 35 files changed, 653 insertions(+), 495 deletions(-) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index 45e15bd715..83bbf1829a 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -20,9 +20,9 @@ Usage Description ----------- -5ttgen acts as a 'master' script for generating a five-tissue-type (5TT) segmented tissue image suitable for use in Anatomically-Constrained Tractography (ACT). A range of different algorithms are available for completing this task. When using this script, the name of the algorithm to be used must appear as the first argument on the command-line after '5ttgen'. The subsequent compulsory arguments and options available depend on the particular algorithm being invoked. +5ttgen acts as a "master" script for generating a five-tissue-type (5TT) segmented tissue image suitable for use in Anatomically-Constrained Tractography (ACT). A range of different algorithms are available for completing this task. When using this script, the name of the algorithm to be used must appear as the first argument on the command-line after "5ttgen". The subsequent compulsory arguments and options available depend on the particular algorithm being invoked. -Each algorithm available also has its own help page, including necessary references; e.g. to see the help page of the 'fsl' algorithm, type '5ttgen fsl'. +Each algorithm available also has its own help page, including necessary references; e.g. to see the help page of the "fsl" algorithm, type "5ttgen fsl". Options ------- @@ -107,13 +107,13 @@ Usage 5ttgen freesurfer input output [ options ] -- *input*: The input FreeSurfer parcellation image (any image containing 'aseg' in its name) +- *input*: The input FreeSurfer parcellation image (any image containing "aseg" in its name) - *output*: The output 5TT image Options ------- -Options specific to the 'freesurfer' algorithm +Options specific to the "freesurfer" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-lut file** Manually provide path to the lookup table on which the input parcellation image is based (e.g. FreeSurferColorLUT.txt) @@ -204,7 +204,7 @@ Usage Options ------- -Options specific to the 'fsl' algorithm +Options specific to the "fsl" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-t2 image** Provide a T2-weighted image in addition to the default T1-weighted image; this will be used as a second input to FSL FAST diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index 6ef9814a03..6325cd6270 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -20,7 +20,7 @@ Usage Description ----------- -This script serves as an interface for many different algorithms that generate a binary mask from DWI data in different ways. Each algorithm available has its own help page, including necessary references; e.g. to see the help page of the 'fslbet' algorithm, type 'dwi2mask fslbet'. +This script serves as an interface for many different algorithms that generate a binary mask from DWI data in different ways. Each algorithm available has its own help page, including necessary references; e.g. to see the help page of the "fslbet" algorithm, type "dwi2mask fslbet". More information on mask derivation from DWI data can be found at the following link: https://mrtrix.readthedocs.io/en/3.0.4/dwi_preprocessing/masking.html @@ -112,16 +112,16 @@ Usage Options ------- -Options specific to the 'afni_3dautomask' algorithm -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Options specific to the "3dautomask" algorithm +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-clfrac** Set the 'clip level fraction', must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger. +- **-clfrac** Set the "clip level fraction"; must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger. -- **-nograd** The program uses a 'gradual' clip level by default. Add this option to use a fixed clip level. +- **-nograd** The program uses a "gradual" clip level by default. Add this option to use a fixed clip level. - **-peels** Peel (erode) the mask n times, then unpeel (dilate). -- **-nbhrs** Define the number of neighbors needed for a voxel NOT to be eroded. It should be between 6 and 26. +- **-nbhrs** Define the number of neighbors needed for a voxel NOT to be eroded. It should be between 6 and 26. - **-eclip** After creating the mask, remove exterior voxels below the clip threshold. @@ -523,7 +523,7 @@ Usage Options ------- -Options specific to the 'fslbet' algorithm +Options specific to the "fslbet" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-bet_f** Fractional intensity threshold (0->1); smaller values give larger brain outline estimates @@ -622,7 +622,7 @@ Usage Options ------- -Options specific to the 'hdbet' algorithm +Options specific to the "hdbet" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-nogpu** Do not attempt to run on the GPU @@ -799,7 +799,7 @@ Usage Options ------- -Options specific to the 'mean' algorithm +Options specific to the "mean" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-shells bvalues** Comma separated list of shells to be included in the volume averaging @@ -904,7 +904,7 @@ Options specific to the "mtnorm" algorithm - **-init_mask image** Provide an initial brain mask, which will constrain the response function estimation (if omitted, the default dwi2mask algorithm will be used) -- **-lmax values** The maximum spherical harmonic degree for the estimated FODs (see Description); defaults are "4,0,0" for multi-shell and "4,0" for single-shell data) +- **-lmax values** The maximum spherical harmonic degree for the estimated FODs (see Description); defaults are "4,0,0" for multi-shell and "4,0" for single-shell data - **-threshold value** the threshold on the total tissue density sum image used to derive the brain mask; default is 0.5 @@ -1102,14 +1102,14 @@ Usage Options ------- -Options for turning 'dwi2mask trace' into an iterative algorithm +Options for turning "dwi2mask trace" into an iterative algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-iterative** (EXPERIMENTAL) Iteratively refine the weights for combination of per-shell trace-weighted images prior to thresholding - **-max_iters** Set the maximum number of iterations for the algorithm (default: 10) -Options specific to the 'trace' algorithm +Options specific to the "trace" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-shells bvalues** Comma-separated list of shells used to generate trace-weighted images for masking diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index dfc28d4d11..ce15f18d44 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -20,14 +20,15 @@ Usage Description ----------- -dwi2response offers different algorithms for performing various types of response function estimation. The name of the algorithm must appear as the first argument on the command-line after 'dwi2response'. The subsequent arguments and options depend on the particular algorithm being invoked. +dwi2response offers different algorithms for performing various types of response function estimation. The name of the algorithm must appear as the first argument on the command-line after "dwi2response". The subsequent arguments and options depend on the particular algorithm being invoked. -Each algorithm available has its own help page, including necessary references; e.g. to see the help page of the 'fa' algorithm, type 'dwi2response fa'. +Each algorithm available has its own help page, including necessary references; e.g. to see the help page of the "fa" algorithm, type "dwi2response fa". More information on response function estimation for spherical deconvolution can be found at the following link: https://mrtrix.readthedocs.io/en/3.0.4/constrained_spherical_deconvolution/response_function_estimation.html -Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to derive an initial voxel exclusion mask. More information on mask derivation from DWI data can be found at: https://mrtrix.readthedocs.io/en/3.0.4/dwi_preprocessing/masking.html +Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to derive an initial voxel exclusion mask. More information on mask derivation from DWI data can be found at: +https://mrtrix.readthedocs.io/en/3.0.4/dwi_preprocessing/masking.html Options ------- @@ -134,7 +135,7 @@ This is an improved version of the Dhollander et al. (2016) algorithm for unsupe Options ------- -Options for the 'dhollander' algorithm +Options for the "dhollander" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-erode passes** Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3) @@ -248,7 +249,7 @@ Usage Options ------- -Options specific to the 'fa' algorithm +Options specific to the "fa" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-erode passes** Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually) @@ -355,7 +356,7 @@ Usage Options ------- -Options specific to the 'manual' algorithm +Options specific to the "manual" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-dirs image** Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise) @@ -458,7 +459,7 @@ Usage Options ------- -Options specific to the 'msmt_5tt' algorithm +Options specific to the "msmt_5tt" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-dirs image** Provide an input image that contains a pre-estimated fibre direction in each voxel (a tensor fit will be used otherwise) @@ -568,7 +569,7 @@ Usage Options ------- -Options specific to the 'tax' algorithm +Options specific to the "tax" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-peak_ratio value** Second-to-first-peak amplitude ratio threshold @@ -674,7 +675,7 @@ Usage Options ------- -Options specific to the 'tournier' algorithm +Options specific to the "tournier" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-number voxels** Number of single-fibre voxels to use when calculating response function diff --git a/docs/reference/commands/dwibiascorrect.rst b/docs/reference/commands/dwibiascorrect.rst index d2ec382651..0cba332966 100644 --- a/docs/reference/commands/dwibiascorrect.rst +++ b/docs/reference/commands/dwibiascorrect.rst @@ -204,7 +204,7 @@ dwibiascorrect fsl Synopsis -------- -Perform DWI bias field correction using the 'fast' command as provided in FSL +Perform DWI bias field correction using the "fast" command as provided in FSL Usage ----- @@ -219,7 +219,7 @@ Usage Description ----------- -The FSL 'fast' command only estimates the bias field within a brain mask, and cannot extrapolate this smoothly-varying field beyond the defined mask. As such, this algorithm by necessity introduces a hard masking of the input DWI. Since this attribute may interfere with the purpose of using the command (e.g. correction of a bias field is commonly used to improve brain mask estimation), use of this particular algorithm is generally not recommended. +The FSL "fast" command only estimates the bias field within a brain mask, and cannot extrapolate this smoothly-varying field beyond the defined mask. As such, this algorithm by necessity introduces a hard masking of the input DWI. Since this attribute may interfere with the purpose of using the command (e.g. correction of a bias field is commonly used to improve brain mask estimation), use of this particular algorithm is generally not recommended. Options ------- diff --git a/docs/reference/commands/dwibiasnormmask.rst b/docs/reference/commands/dwibiasnormmask.rst index c8ffd5b80e..7016a43c59 100644 --- a/docs/reference/commands/dwibiasnormmask.rst +++ b/docs/reference/commands/dwibiasnormmask.rst @@ -26,13 +26,13 @@ DWI bias field correction, intensity normalisation and masking are inter-related The operation of the algorithm is as follows. An initial mask is defined, either using the default dwi2mask algorithm or as provided by the user. Based on this mask, a sequence of response function estimation, multi-shell multi-tissue CSD, bias field correction (using the mtnormalise command), and intensity normalisation is performed. The default dwi2mask algorithm is then re-executed on the bias-field-corrected DWI series. This sequence of steps is then repeated based on the revised mask, until either a convergence criterion or some number of maximum iterations is reached. -The MRtrix3 mtnormalise command is used to estimate information relating to bias field and intensity normalisation. However its usage in this context is different to its conventional usage. Firstly, while the corrected ODF images are typically used directly following invocation of this command, here the estimated bias field and scaling factors are instead used to apply the relevant corrections to the originating DWI data. Secondly, the global intensity scaling that is calculated and applied is typically based on achieving close to a unity sum of tissue signal fractions throughout the masked region. Here, it is instead the b=0 signal in CSF that forms the reference for this global intensity scaling; this is calculated based on the estimated CSF response function and the tissue-specific intensity scaling (this is calculated internally by mtnormalise as part of its optimisation process, but typically subsequently discarded in favour of a single scaling factor for all tissues) +The MRtrix3 mtnormalise command is used to estimate information relating to bias field and intensity normalisation. However its usage in this context is different to its conventional usage. Firstly, while the corrected ODF images are typically used directly following invocation of this command here the estimated bias field and scaling factors are instead used to apply the relevant corrections to the originating DWI data. Secondly, the global intensity scaling that is calculated and applied is typically based on achieving close to a unity sum of tissue signal fractions throughout the masked region. Here, it is instead the b=0 signal in CSF that forms the reference for this global intensity scaling; this is calculated based on the estimated CSF response function and the tissue-specific intensity scaling (this is calculated internally by mtnormalise as part of its optimisation process, but typically subsequently discarded in favour of a single scaling factor for all tissues) The ODFs estimated within this optimisation procedure are by default of lower maximal spherical harmonic degree than what would be advised for analysis. This is done for computational efficiency. This behaviour can be modified through the -lmax command-line option. By default, the optimisation procedure will terminate after only two iterations. This is done because it has been observed for some data / configurations that additional iterations can lead to unstable divergence and erroneous results for bias field estimation and masking. For other configurations, it may be preferable to use a greater number of iterations, and allow the iterative algorithm to converge to a stable solution. This can be controlled via the -max_iters command-line option. -Within the optimisation algorithm, derivation of the mask may potentially be performed differently to a conventional mask derivation that is based on a DWI series (where, in many instances, it is actually only the mean b=0 image that is used). Here, the image corresponding to the sum of tissue signal fractions following spherical deconvolution / bias field correction / intensity normalisation is also available, and this can potentially be used for mask derivation. Available options are as follows. "dwi2mask": Use the MRtrix3 command dwi2mask on the bias-field-corrected DWI series (ie. do not use the ODF tissue sum image for mask derivation); the algorithm to be invoked can be controlled by the user via the MRtrix config file entry "Dwi2maskAlgorithm". "fslbet": Invoke the FSL command "bet" on the ODF tissue sum image. "hdbet": Invoke the HD-BET command on the ODF tissue sum image. "mrthreshold": Invoke the MRtrix3 command "mrthreshold" on the ODF tissue sum image, where an appropriate threshold value will be determined automatically (and some heuristic cleanup of the resulting mask will be performed). "synthstrip": Invoke the FreeSurfer SynthStrip method on the ODF tissue sum image. "threshold": Apply a fixed partial volume threshold of 0.5 to the ODF tissue sum image (and some heuristic cleanup of the resulting mask will be performed). +Within the optimisation algorithm, derivation of the mask may potentially be performed differently to a conventional mask derivation that is based on a DWI series (where, in many instances, it is actually only the mean b=0 image that is used). Here, the image corresponding to the sum of tissue signal fractions following spherical deconvolution / bias field correction / intensity normalisation is also available, and this can potentially be used for mask derivation. Available options are as follows. "dwi2mask": Use the MRtrix3 command dwi2mask on the bias-field-corrected DWI series (ie. do not use the ODF tissue sum image for mask derivation); the algorithm to be invoked can be controlled by the user via the MRtrix config file entry "Dwi2maskAlgorithm". "fslbet": Invoke the FSL command "bet" on the ODF tissue sum image. "hdbet": Invoke the HD-BET command on the ODF tissue sum image. "mrthreshold": Invoke the MRtrix3 command "mrthreshold" on the ODF tissue sum image, where an appropriate threshold value will be determined automatically (and some heuristic cleanup of the resulting mask will be performed). "synthstrip": Invoke the FreeSurfer SynthStrip method on the ODF tissue sum image. "threshold": Apply a fixed partial volume threshold of 0.5 to the ODF tissue sum image (and some heuristic cleanup of the resulting mask will be performed). Options ------- @@ -47,24 +47,24 @@ Options for importing the diffusion gradient table Options relevant to the internal optimisation procedure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-dice value** Set the Dice coefficient threshold for similarity of masks between sequential iterations that will result in termination due to convergence; default = 0.999 +- **-dice value** Set the Dice coefficient threshold for similarity of masks between sequential iterations that will result in termination due to convergence; default = 0.999 -- **-init_mask image** Provide an initial mask for the first iteration of the algorithm (if not provided, the default dwi2mask algorithm will be used) +- **-init_mask** Provide an initial mask for the first iteration of the algorithm (if not provided, the default dwi2mask algorithm will be used) - **-max_iters count** The maximum number of iterations (see Description); default is 2; set to 0 to proceed until convergence - **-mask_algo algorithm** The algorithm to use for mask estimation, potentially based on the ODF sum image (see Description); default: threshold -- **-lmax values** The maximum spherical harmonic degree for the estimated FODs (see Description); defaults are "4,0,0" for multi-shell and "4,0" for single-shell data) +- **-lmax values** The maximum spherical harmonic degree for the estimated FODs (see Description); defaults are "4,0,0" for multi-shell and "4,0" for single-shell data) Options that modulate the outputs of the script ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-output_bias image** Export the final estimated bias field to an image +- **-output_bias** Export the final estimated bias field to an image -- **-output_scale file** Write the scaling factor applied to the DWI series to a text file +- **-output_scale** Write the scaling factor applied to the DWI series to a text file -- **-output_tissuesum image** Export the tissue sum image that was used to generate the final mask +- **-output_tissuesum** Export the tissue sum image that was used to generate the final mask - **-reference value** Set the target CSF b=0 intensity in the output DWI series (default: 1000.0) diff --git a/docs/reference/commands/dwifslpreproc.rst b/docs/reference/commands/dwifslpreproc.rst index d0c3a02faf..81927c4f0a 100644 --- a/docs/reference/commands/dwifslpreproc.rst +++ b/docs/reference/commands/dwifslpreproc.rst @@ -26,13 +26,16 @@ This script is intended to provide convenience of use of the FSL software tools More information on use of the dwifslpreproc command can be found at the following link: https://mrtrix.readthedocs.io/en/3.0.4/dwi_preprocessing/dwifslpreproc.html -Note that the MRtrix3 command dwi2mask will automatically be called to derive a processing mask for the FSL command "eddy", which determines which voxels contribute to the estimation of geometric distortion parameters and possibly also the classification of outlier slices. If FSL command "topup" is used to estimate a susceptibility field, then dwi2mask will be executed on the resuts of running FSL command "applytopup" to the input DWIs; otherwise it will be executed directly on the input DWIs. Alternatively, the -eddy_mask option can be specified in order to manually provide such a processing mask. More information on mask derivation from DWI data can be found at: https://mrtrix.readthedocs.io/en/3.0.4/dwi_preprocessing/masking.html +Note that the MRtrix3 command dwi2mask will automatically be called to derive a processing mask for the FSL command "eddy", which determines which voxels contribute to the estimation of geometric distortion parameters and possibly also the classification of outlier slices. If FSL command "topup" is used to estimate a susceptibility field, then dwi2mask will be executed on the resuts of running FSL command "applytopup" to the input DWIs; otherwise it will be executed directly on the input DWIs. Alternatively, the -eddy_mask option can be specified in order to manually provide such a processing mask. More information on mask derivation from DWI data can be found at: +https://mrtrix.readthedocs.io/en/3.0.4/dwi_preprocessing/masking.html -The "-topup_options" and "-eddy_options" command-line options allow the user to pass desired command-line options directly to the FSL commands topup and eddy. The available options for those commands may vary between versions of FSL; users can interrogate such by querying the help pages of the installed software, and/or the FSL online documentation: (topup) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide ; (eddy) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide +The "-topup_options" and "-eddy_options" command-line options allow the user to pass desired command-line options directly to the FSL commands topup and eddy. The available options for those commands may vary between versions of FSL; users can interrogate such by querying the help pages of the installed software, and/or the FSL online documentation: +(topup) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide ; +(eddy) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide The script will attempt to run the CUDA version of eddy; if this does not succeed for any reason, or is not present on the system, the CPU version will be attempted instead. By default, the CUDA eddy binary found that indicates compilation against the most recent version of CUDA will be attempted; this can be over-ridden by providing a soft-link "eddy_cuda" within your path that links to the binary you wish to be executed. -Note that this script does not perform any explicit registration between images provided to topup via the -se_epi option, and the DWI volumes provided to eddy. In some instances (motion between acquisitions) this can result in erroneous application of the inhomogeneity field during distortion correction. Use of the -align_seepi option is advocated in this scenario, which ensures that the first volume in the series provided to topup is also the first volume in the series provided to eddy, guaranteeing alignment. But a prerequisite for this approach is that the image contrast within the images provided to the -se_epi option must match the b=0 volumes present within the input DWI series: this means equivalent TE, TR and flip angle (note that differences in multi-band factors between two acquisitions may lead to differences in TR). +Note that this script does not perform any explicit registration between images provided to topup via the -se_epi option, and the DWI volumes provided to eddy. In some instances (motion between acquisitions) this can result in erroneous application of the inhomogeneity field during distortion correction. Use of the -align_seepi option is advocated in this scenario, which ensures that the first volume in the series provided to topup is also the first volume in the series provided to eddy, guaranteeing alignment. But a prerequisite for this approach is that the image contrast within the images provided to the -se_epi option must match the b=0 volumes present within the input DWI series: this means equivalent TE, TR and flip angle (note that differences in multi-band factors between two acquisitions may lead to differences in TR). Example usages -------------- @@ -59,7 +62,7 @@ Example usages $ mrcat DWI_*.mif DWI_all.mif -axis 3; mrcat b0_*.mif b0_all.mif -axis 3; dwifslpreproc DWI_all.mif DWI_out.mif -rpe_header -se_epi b0_all.mif -align_seepi - With this usage, the relevant phase encoding information is determined entirely based on the contents of the relevant image headers, and dwifslpreproc prepares all metadata for the executed FSL commands accordingly. This can therefore be used if the particular DWI acquisition strategy used does not correspond to one of the simple examples as described in the prior examples. This usage is predicated on the headers of the input files containing appropriately-named key-value fields such that MRtrix3 tools identify them as such. In some cases, conversion from DICOM using MRtrix3 commands will automatically extract and embed this information; however this is not true for all scanner vendors and/or software versions. In the latter case it may be possible to manually provide these metadata; either using the -json_import command-line option of dwifslpreproc, or the -json_import or one of the -import_pe_* command-line options of MRtrix3's mrconvert command (and saving in .mif format) prior to running dwifslpreproc. + With this usage, the relevant phase encoding information is determined entirely based on the contents of the relevant image headers, and dwifslpreproc prepares all metadata for the executed FSL commands accordingly. This can therefore be used if the particular DWI acquisition strategy used does not correspond to one of the simple examples as described in the prior examples. This usage is predicated on the headers of the input files containing appropriately-named key-value fields such that MRtrix3 tools identify them as such. In some cases, conversion from DICOM using MRtrix3 commands will automatically extract and embed this information; however this is not true for all scanner vendors and/or software versions. In the latter case it may be possible to manually provide these metadata; either using the -json_import command-line option of dwifslpreproc, or the -json_import or one of the -import_pe_* command-line options of MRtrix3's mrconvert command (and saving in .mif format) prior to running dwifslpreproc. Options ------- @@ -112,7 +115,7 @@ Options for achieving correction of susceptibility distortions - **-se_epi image** Provide an additional image series consisting of spin-echo EPI images, which is to be used exclusively by topup for estimating the inhomogeneity field (i.e. it will not form part of the output image series) -- **-align_seepi** Achieve alignment between the SE-EPI images used for inhomogeneity field estimation, and the DWIs (more information in Description section) +- **-align_seepi** Achieve alignment between the SE-EPI images used for inhomogeneity field estimation and the DWIs (more information in Description section) - **-topup_options " TopupOptions"** Manually provide additional command-line options to the topup command (provide a string within quotation marks that contains at least one space, even if only passing a single command-line option to topup) diff --git a/docs/reference/commands/dwishellmath.rst b/docs/reference/commands/dwishellmath.rst index 4c6611b63f..005820818b 100644 --- a/docs/reference/commands/dwishellmath.rst +++ b/docs/reference/commands/dwishellmath.rst @@ -22,7 +22,7 @@ Usage Description ----------- -The output of this command is a 4D image, where each volume corresponds to a b-value shell (in order of increasing b-value), and the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell. +The output of this command is a 4D image, where each volume corresponds to a b-value shell (in order of increasing b-value), an the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell. Example usages -------------- diff --git a/docs/reference/commands/for_each.rst b/docs/reference/commands/for_each.rst index c1375d0395..b09948c876 100644 --- a/docs/reference/commands/for_each.rst +++ b/docs/reference/commands/for_each.rst @@ -48,13 +48,13 @@ Example usages $ for_each folder/*.mif : mrinfo IN - This will run the "mrinfo" command for every .mif file present in "folder/". Note that the compulsory colon symbol is used to separate the list of items on which for_each is being instructed to operate, from the command that is intended to be run for each input. + This will run the "mrinfo" command for every .mif file present in "folder/". Note that the compulsory colon symbol is used to separate the list of items on which for_each is being instructed to operate from the command that is intended to be run for each input. - *Multi-threaded use of for_each*:: $ for_each -nthreads 4 freesurfer/subjects/* : recon-all -subjid NAME -all - In this example, for_each is instructed to run the FreeSurfer command 'recon-all' for all subjects within the 'subjects' directory, with four subjects being processed in parallel at any one time. Whenever processing of one subject is completed, processing for a new unprocessed subject will commence. This technique is useful for improving the efficiency of running single-threaded commands on multi-core systems, as long as the system possesses enough memory to support such parallel processing. Note that in the case of multi-threaded commands (which includes many MRtrix3 commands), it is generally preferable to permit multi-threaded execution of the command on a single input at a time, rather than processing multiple inputs in parallel. + In this example, for_each is instructed to run the FreeSurfer command "recon-all" for all subjects within the "subjects" directory, with four subjects being processed in parallel at any one time. Whenever processing of one subject is completed, processing for a new unprocessed subject will commence. This technique is useful for improving the efficiency of running single-threaded commands on multi-core systems, as long as the system possesses enough memory to support such parallel processing. Note that in the case of multi-threaded commands (which includes many MRtrix3 commands), it is generally preferable to permit multi-threaded execution of the command on a single input at a time, rather than processing multiple inputs in parallel. - *Excluding specific inputs from execution*:: diff --git a/docs/reference/commands/population_template.rst b/docs/reference/commands/population_template.rst index e9b64da171..9e38019957 100644 --- a/docs/reference/commands/population_template.rst +++ b/docs/reference/commands/population_template.rst @@ -38,17 +38,17 @@ Options Input, output and general options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-type** Specify the types of registration stages to perform. Options are "rigid" (perform rigid registration only which might be useful for intra-subject registration in longitudinal analysis), "affine" (perform affine registration) and "nonlinear" as well as cominations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear +- **-type** Specify the types of registration stages to perform. Options are: "rigid" (perform rigid registration only, which might be useful for intra-subject registration in longitudinal analysis); "affine" (perform affine registration); "nonlinear"; as well as cominations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear - **-voxel_size** Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values. -- **-initial_alignment** Method of alignment to form the initial template. Options are "mass" (default), "robust_mass" (requires masks), "geometric" and "none". +- **-initial_alignment** Method of alignment to form the initial template.Options are: "mass" (default); "robust_mass" (requires masks); "geometric"; "none". - **-mask_dir** Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images. - **-warp_dir** Output a directory containing warps from each input to the template. If the folder does not exist it will be created -- **-transformed_dir** Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide comma separated list of directories. +- **-transformed_dir** Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, this path will contain a sub-directory for the images per contrast. - **-linear_transformations_dir** Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created @@ -56,7 +56,7 @@ Input, output and general options - **-noreorientation** Turn off FOD reorientation in mrregister. Reorientation is on by default if the number of volumes in the 4th dimension corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series (i.e. 6, 15, 28, 45, 66 etc) -- **-leave_one_out** Register each input image to a template that does not contain that image. Valid choices: 0, 1, auto. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) +- **-leave_one_out** Register each input image to a template that does not contain that image. Valid choices: 0, 1, auto. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) - **-aggregate** Measure used to aggregate information from transformed images to the template image. Valid choices: mean, median. Default: mean @@ -71,7 +71,7 @@ Input, output and general options Options for the non-linear registration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-nl_scale** Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: 0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0). This implicitly defines the number of template levels +- **-nl_scale** Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0). This implicitly defines the number of template levels - **-nl_lmax** Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: 2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4). The list must be the same length as the nl_scale factor list @@ -96,24 +96,24 @@ Options for the linear registration - **-rigid_lmax** Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: 2,2,2,4,4,4). The list must be the same length as the linear_scale factor list -- **-rigid_niter** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:50 for each scale). This must be a single number or a list of same length as the linear_scale factor list +- **-rigid_niter** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 50 for each scale). This must be a single number or a list of same length as the linear_scale factor list - **-affine_scale** Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: 0.3,0.4,0.6,0.8,1.0,1.0). This and rigid_scale implicitly define the number of template levels - **-affine_lmax** Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: 2,2,2,4,4,4). The list must be the same length as the linear_scale factor list -- **-affine_niter** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default:500 for each scale). This must be a single number or a list of same length as the linear_scale factor list +- **-affine_niter** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 500 for each scale). This must be a single number or a list of same length as the linear_scale factor list Multi-contrast options ^^^^^^^^^^^^^^^^^^^^^^ -- **-mc_weight_initial_alignment** Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0 +- **-mc_weight_initial_alignment** Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0 for each contrast (ie. equal weighting). -- **-mc_weight_rigid** Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0 +- **-mc_weight_rigid** Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) -- **-mc_weight_affine** Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0 +- **-mc_weight_affine** Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) -- **-mc_weight_nl** Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0 +- **-mc_weight_nl** Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -152,7 +152,7 @@ Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; Pietsch -**Author:** David Raffelt (david.raffelt@florey.edu.au) & Max Pietsch (maximilian.pietsch@kcl.ac.uk) & Thijs Dhollander (thijs.dhollander@gmail.com) +**Author:** David Raffelt (david.raffelt@florey.edu.au) and Max Pietsch (maximilian.pietsch@kcl.ac.uk) and Thijs Dhollander (thijs.dhollander@gmail.com) **Copyright:** Copyright (c) 2008-2024 the MRtrix3 contributors. diff --git a/docs/reference/commands/responsemean.rst b/docs/reference/commands/responsemean.rst index 1036ea8272..3d5663cbfb 100644 --- a/docs/reference/commands/responsemean.rst +++ b/docs/reference/commands/responsemean.rst @@ -21,16 +21,29 @@ Usage Description ----------- -Example usage: responsemean input_response1.txt input_response2.txt input_response3.txt ... output_average_response.txt - All response function files provided must contain the same number of unique b-values (lines), as well as the same number of coefficients per line. -As long as the number of unique b-values is identical across all input files, the coefficients will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the responsemean command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied. +As long as the number of unique b-values is identical across all input files, the response functions will be averaged. This is performed on the assumption that the actual acquired b-values are identical. This is however impossible for the responsemean command to determine based on the data provided; it is therefore up to the user to ensure that this requirement is satisfied. + +Example usages +-------------- + +- *Usage where all response functions are in the same directory:*:: + + $ responsemean input_response1.txt input_response2.txt input_response3.txt output_average_response.txt + +- *Usage selecting response functions within a directory using a wildcard:*:: + + $ responsemean input_response*.txt output_average_response.txt + +- *Usage where data for each participant reside in a participant-specific directory:*:: + + $ responsemean subject-*/response.txt output_average_response.txt Options ------- -- **-legacy** Use the legacy behaviour of former command 'average_response': average response function coefficients directly, without compensating for global magnitude differences between input files +- **-legacy** Use the legacy behaviour of former command "average_response": average response function coefficients directly, without compensating for global magnitude differences between input files Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/python/bin/5ttgen b/python/bin/5ttgen index ccb5522d56..8e5d962899 100755 --- a/python/bin/5ttgen +++ b/python/bin/5ttgen @@ -23,14 +23,21 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Generate a 5TT image suitable for ACT') cmdline.add_citation('Smith, R. E.; Tournier, J.-D.; Calamante, F. & Connelly, A. ' - 'Anatomically-constrained tractography: Improved diffusion MRI streamlines tractography through effective use of anatomical information. ' + 'Anatomically-constrained tractography:' + ' Improved diffusion MRI streamlines tractography through effective use of anatomical information. ' 'NeuroImage, 2012, 62, 1924-1938') - cmdline.add_description('5ttgen acts as a "master" script for generating a five-tissue-type (5TT) segmented tissue image suitable for use in Anatomically-Constrained Tractography (ACT). ' - 'A range of different algorithms are available for completing this task. ' - 'When using this script, the name of the algorithm to be used must appear as the first argument on the command-line after "5ttgen". ' - 'The subsequent compulsory arguments and options available depend on the particular algorithm being invoked.') - cmdline.add_description('Each algorithm available also has its own help page, including necessary references; ' - 'e.g. to see the help page of the "fsl" algorithm, type "5ttgen fsl".') + cmdline.add_description('5ttgen acts as a "master" script' + ' for generating a five-tissue-type (5TT) segmented tissue image' + ' suitable for use in Anatomically-Constrained Tractography (ACT).' + ' A range of different algorithms are available for completing this task.' + ' When using this script,' + ' the name of the algorithm to be used must appear' + ' as the first argument on the command-line after "5ttgen".' + ' The subsequent compulsory arguments and options available' + ' depend on the particular algorithm being invoked.') + cmdline.add_description('Each algorithm available also has its own help page,' + ' including necessary references;' + ' e.g. to see the help page of the "fsl" algorithm, type "5ttgen fsl".') common_options = cmdline.add_argument_group('Options common to all 5ttgen algorithms') common_options.add_argument('-nocrop', diff --git a/python/bin/dwi2mask b/python/bin/dwi2mask index a1b11031aa..ab805cc103 100755 --- a/python/bin/dwi2mask +++ b/python/bin/dwi2mask @@ -20,11 +20,15 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import algorithm, app, _version #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Warda Syeda (wtsyeda@unimelb.edu.au)') + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)' + ' and Warda Syeda (wtsyeda@unimelb.edu.au)') cmdline.set_synopsis('Generate a binary mask from DWI data') - cmdline.add_description('This script serves as an interface for many different algorithms that generate a binary mask from DWI data in different ways. ' - 'Each algorithm available has its own help page, including necessary references; ' - 'e.g. to see the help page of the "fslbet" algorithm, type "dwi2mask fslbet".') + cmdline.add_description('This script serves as an interface for many different algorithms' + ' that generate a binary mask from DWI data in different ways.' + ' Each algorithm available has its own help page,' + ' including necessary references;' + ' e.g. to see the help page of the "fslbet" algorithm,' + ' type "dwi2mask fslbet".') cmdline.add_description('More information on mask derivation from DWI data can be found at the following link: \n' f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') diff --git a/python/bin/dwi2response b/python/bin/dwi2response index 8c8e8687d6..c1e1a5f53e 100755 --- a/python/bin/dwi2response +++ b/python/bin/dwi2response @@ -20,18 +20,23 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import algorithm, app, _version #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Thijs Dhollander (thijs.dhollander@gmail.com)') + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)' + ' and Thijs Dhollander (thijs.dhollander@gmail.com)') cmdline.set_synopsis('Estimate response function(s) for spherical deconvolution') cmdline.add_description('dwi2response offers different algorithms for performing various types of response function estimation. ' 'The name of the algorithm must appear as the first argument on the command-line after "dwi2response". ' 'The subsequent arguments and options depend on the particular algorithm being invoked.') - cmdline.add_description('Each algorithm available has its own help page, including necessary references; ' - 'e.g. to see the help page of the "fa" algorithm, type "dwi2response fa".') - cmdline.add_description('More information on response function estimation for spherical deconvolution can be found at the following link: \n' + cmdline.add_description('Each algorithm available has its own help page,' + ' including necessary references;' + ' e.g. to see the help page of the "fa" algorithm,' + ' type "dwi2response fa".') + cmdline.add_description('More information on response function estimation for spherical deconvolution' + ' can be found at the following link: \n' f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/constrained_spherical_deconvolution/response_function_estimation.html') - cmdline.add_description('Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to ' - 'derive an initial voxel exclusion mask. ' - 'More information on mask derivation from DWI data can be found at: ' + cmdline.add_description('Note that if the -mask command-line option is not specified,' + ' the MRtrix3 command dwi2mask will automatically be called to' + ' derive an initial voxel exclusion mask.' + ' More information on mask derivation from DWI data can be found at: \n' f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') # General options diff --git a/python/bin/dwibiasnormmask b/python/bin/dwibiasnormmask index 3559c2aecf..e5c4e91dc6 100755 --- a/python/bin/dwibiasnormmask +++ b/python/bin/dwibiasnormmask @@ -27,50 +27,70 @@ DICE_COEFF_DEFAULT = 1.0 - 1e-3 def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)' + ' and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') cmdline.set_synopsis('Perform a combination of bias field correction, intensity normalisation, and mask derivation, for DWI data') - cmdline.add_description('DWI bias field correction, intensity normalisation and masking are inter-related steps, and errors ' - 'in each may influence other steps. This script is designed to perform all of these steps in an integrated ' - 'iterative fashion, with the intention of making all steps more robust.') - cmdline.add_description('The operation of the algorithm is as follows. An initial mask is defined, either using the default dwi2mask ' - 'algorithm or as provided by the user. Based on this mask, a sequence of response function estimation, ' - 'multi-shell multi-tissue CSD, bias field correction (using the mtnormalise command), and intensity ' - 'normalisation is performed. The default dwi2mask algorithm is then re-executed on the bias-field-corrected ' - 'DWI series. This sequence of steps is then repeated based on the revised mask, until either a convergence ' - 'criterion or some number of maximum iterations is reached.') - cmdline.add_description('The MRtrix3 mtnormalise command is used to estimate information relating to bias field and intensity ' - 'normalisation. However its usage in this context is different to its conventional usage. Firstly, ' - 'while the corrected ODF images are typically used directly following invocation of this command, ' - 'here the estimated bias field and scaling factors are instead used to apply the relevant corrections to ' - 'the originating DWI data. Secondly, the global intensity scaling that is calculated and applied is ' - 'typically based on achieving close to a unity sum of tissue signal fractions throughout the masked region. ' - 'Here, it is instead the b=0 signal in CSF that forms the reference for this global intensity scaling; ' - 'this is calculated based on the estimated CSF response function and the tissue-specific intensity ' - 'scaling (this is calculated internally by mtnormalise as part of its optimisation process, but typically ' - 'subsequently discarded in favour of a single scaling factor for all tissues)') - cmdline.add_description('The ODFs estimated within this optimisation procedure are by default of lower maximal spherical harmonic ' - 'degree than what would be advised for analysis. This is done for computational efficiency. This ' - 'behaviour can be modified through the -lmax command-line option.') - cmdline.add_description('By default, the optimisation procedure will terminate after only two iterations. This is done because ' - 'it has been observed for some data / configurations that additional iterations can lead to unstable ' - 'divergence and erroneous results for bias field estimation and masking. For other configurations, ' - 'it may be preferable to use a greater number of iterations, and allow the iterative algorithm to ' - 'converge to a stable solution. This can be controlled via the -max_iters command-line option.') - cmdline.add_description('Within the optimisation algorithm, derivation of the mask may potentially be performed differently to ' - 'a conventional mask derivation that is based on a DWI series (where, in many instances, it is actually ' - 'only the mean b=0 image that is used). Here, the image corresponding to the sum of tissue signal fractions ' - 'following spherical deconvolution / bias field correction / intensity normalisation is also available, ' - 'and this can potentially be used for mask derivation. Available options are as follows. ' - '"dwi2mask": Use the MRtrix3 command dwi2mask on the bias-field-corrected DWI series ' - '(ie. do not use the ODF tissue sum image for mask derivation); ' - 'the algorithm to be invoked can be controlled by the user via the MRtrix config file entry "Dwi2maskAlgorithm". ' - '"fslbet": Invoke the FSL command "bet" on the ODF tissue sum image. ' - '"hdbet": Invoke the HD-BET command on the ODF tissue sum image. ' - '"mrthreshold": Invoke the MRtrix3 command "mrthreshold" on the ODF tissue sum image, ' - 'where an appropriate threshold value will be determined automatically ' - '(and some heuristic cleanup of the resulting mask will be performed). ' - '"synthstrip": Invoke the FreeSurfer SynthStrip method on the ODF tissue sum image. ' - '"threshold": Apply a fixed partial volume threshold of 0.5 to the ODF tissue sum image ' + cmdline.add_description('DWI bias field correction,' + ' intensity normalisation and masking are inter-related steps,' + ' and errors in each may influence other steps.' + ' This script is designed to perform all of these steps in an integrated iterative fashion,' + ' with the intention of making all steps more robust.') + cmdline.add_description('The operation of the algorithm is as follows.' + ' An initial mask is defined,' + ' either using the default dwi2mask algorithm or as provided by the user.' + ' Based on this mask,' + ' a sequence of response function estimation, ' + 'multi-shell multi-tissue CSD,' + ' bias field correction (using the mtnormalise command),' + ' and intensity normalisation is performed.' + ' The default dwi2mask algorithm is then re-executed on the bias-field-corrected DWI series.' + ' This sequence of steps is then repeated based on the revised mask,' + ' until either a convergence criterion or some number of maximum iterations is reached.') + cmdline.add_description('The MRtrix3 mtnormalise command is used to estimate information' + ' relating to bias field and intensity normalisation.' + ' However its usage in this context is different to its conventional usage.' + ' Firstly, while the corrected ODF images are typically used directly following invocation of this command' + ' here the estimated bias field and scaling factors are instead used' + ' to apply the relevant corrections to the originating DWI data.' + ' Secondly, the global intensity scaling that is calculated and applied is typically based' + ' on achieving close to a unity sum of tissue signal fractions throughout the masked region.' + ' Here, it is instead the b=0 signal in CSF that forms the reference for this global intensity scaling;' + ' this is calculated based on the estimated CSF response function' + ' and the tissue-specific intensity scaling' + ' (this is calculated internally by mtnormalise as part of its optimisation process,' + ' but typically subsequently discarded in favour of a single scaling factor for all tissues)') + cmdline.add_description('The ODFs estimated within this optimisation procedure are by default' + ' of lower maximal spherical harmonic degree than what would be advised for analysis.' + ' This is done for computational efficiency.' + ' This behaviour can be modified through the -lmax command-line option.') + cmdline.add_description('By default, the optimisation procedure will terminate after only two iterations.' + ' This is done because it has been observed for some data / configurations that' + ' additional iterations can lead to unstable divergence' + ' and erroneous results for bias field estimation and masking.' + ' For other configurations,' + ' it may be preferable to use a greater number of iterations,' + ' and allow the iterative algorithm to converge to a stable solution.' + ' This can be controlled via the -max_iters command-line option.') + cmdline.add_description('Within the optimisation algorithm,' + ' derivation of the mask may potentially be performed' + ' differently to a conventional mask derivation that is based on a DWI series' + ' (where, in many instances, it is actually only the mean b=0 image that is used).' + ' Here, the image corresponding to the sum of tissue signal fractions' + ' following spherical deconvolution / bias field correction / intensity normalisation' + ' is also available, ' + ' and this can potentially be used for mask derivation.' + ' Available options are as follows.' + ' "dwi2mask": Use the MRtrix3 command dwi2mask on the bias-field-corrected DWI series' + ' (ie. do not use the ODF tissue sum image for mask derivation);' + ' the algorithm to be invoked can be controlled by the user' + ' via the MRtrix config file entry "Dwi2maskAlgorithm".' + ' "fslbet": Invoke the FSL command "bet" on the ODF tissue sum image.' + ' "hdbet": Invoke the HD-BET command on the ODF tissue sum image.' + ' "mrthreshold": Invoke the MRtrix3 command "mrthreshold" on the ODF tissue sum image,' + ' where an appropriate threshold value will be determined automatically' + ' (and some heuristic cleanup of the resulting mask will be performed).' + ' "synthstrip": Invoke the FreeSurfer SynthStrip method on the ODF tissue sum image.' + ' "threshold": Apply a fixed partial volume threshold of 0.5 to the ODF tissue sum image' ' (and some heuristic cleanup of the resulting mask will be performed).') cmdline.add_citation('Jeurissen, B; Tournier, J-D; Dhollander, T; Connelly, A & Sijbers, J. ' 'Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. ' @@ -96,47 +116,54 @@ def usage(cmdline): #pylint: disable=unused-variable output_options = cmdline.add_argument_group('Options that modulate the outputs of the script') output_options.add_argument('-output_bias', type=app.Parser.ImageOut(), + metavar='image', help='Export the final estimated bias field to an image') output_options.add_argument('-output_scale', type=app.Parser.FileOut(), + metavar='file', help='Write the scaling factor applied to the DWI series to a text file') output_options.add_argument('-output_tissuesum', type=app.Parser.ImageOut(), + metavar='image', help='Export the tissue sum image that was used to generate the final mask') output_options.add_argument('-reference', type=app.Parser.Float(0.0), metavar='value', default=REFERENCE_INTENSITY, - help=f'Set the target CSF b=0 intensity in the output DWI series (default: {REFERENCE_INTENSITY})') + help='Set the target CSF b=0 intensity in the output DWI series' + f' (default: {REFERENCE_INTENSITY})') internal_options = cmdline.add_argument_group('Options relevant to the internal optimisation procedure') internal_options.add_argument('-dice', type=app.Parser.Float(0.0, 1.0), default=DICE_COEFF_DEFAULT, metavar='value', - help=f'Set the Dice coefficient threshold for similarity of masks between sequential iterations that will ' - f'result in termination due to convergence; default = {DICE_COEFF_DEFAULT}') + help='Set the Dice coefficient threshold for similarity of masks between sequential iterations' + ' that will result in termination due to convergence;' + f' default = {DICE_COEFF_DEFAULT}') internal_options.add_argument('-init_mask', type=app.Parser.ImageIn(), - help='Provide an initial mask for the first iteration of the algorithm ' - '(if not provided, the default dwi2mask algorithm will be used)') + metavar='image', + help='Provide an initial mask for the first iteration of the algorithm' + ' (if not provided, the default dwi2mask algorithm will be used)') internal_options.add_argument('-max_iters', type=app.Parser.Int(0), default=DWIBIASCORRECT_MAX_ITERS, metavar='count', - help=f'The maximum number of iterations (see Description); default is {DWIBIASCORRECT_MAX_ITERS}; ' - f'set to 0 to proceed until convergence') + help='The maximum number of iterations (see Description);' + f' default is {DWIBIASCORRECT_MAX_ITERS};' + ' set to 0 to proceed until convergence') internal_options.add_argument('-mask_algo', choices=MASK_ALGOS, metavar='algorithm', - help=f'The algorithm to use for mask estimation, ' - f'potentially based on the ODF sum image (see Description); ' - f'default: {MASK_ALGO_DEFAULT}') + help='The algorithm to use for mask estimation,' + ' potentially based on the ODF sum image (see Description);' + f' default: {MASK_ALGO_DEFAULT}') internal_options.add_argument('-lmax', metavar='values', type=app.Parser.SequenceInt(), - help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' - f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' - f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data)') + help='The maximum spherical harmonic degree for the estimated FODs (see Description);' + f' defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' + f' and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data)') app.add_dwgrad_import_options(cmdline) diff --git a/python/bin/dwicat b/python/bin/dwicat index e7ede9a896..810fe14ead 100755 --- a/python/bin/dwicat +++ b/python/bin/dwicat @@ -24,15 +24,16 @@ import json, shutil def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Lena Dorfschmidt (ld548@cam.ac.uk) ' - 'and Jakub Vohryzek (jakub.vohryzek@queens.ox.ac.uk) ' - 'and Robert E. Smith (robert.smith@florey.edu.au)') + cmdline.set_author('Lena Dorfschmidt (ld548@cam.ac.uk)' + ' and Jakub Vohryzek (jakub.vohryzek@queens.ox.ac.uk)' + ' and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Concatenating multiple DWI series accounting for differential intensity scaling') - cmdline.add_description('This script concatenates two or more 4D DWI series, accounting for the ' - 'fact that there may be differences in intensity scaling between those series. ' - 'This intensity scaling is corrected by determining scaling factors that will ' - 'make the overall image intensities in the b=0 volumes of each series approximately ' - 'equivalent.') + cmdline.add_description('This script concatenates two or more 4D DWI series,' + ' accounting for the fact that there may be differences in' + ' intensity scaling between those series.' + ' This intensity scaling is corrected by determining scaling factors' + ' that will make the overall image intensities in the b=0 volumes' + ' of each series approximately equivalent.') cmdline.add_argument('inputs', nargs='+', type=app.Parser.ImageIn(), diff --git a/python/bin/dwifslpreproc b/python/bin/dwifslpreproc index 313c4ed385..fd90aa5ae3 100755 --- a/python/bin/dwifslpreproc +++ b/python/bin/dwifslpreproc @@ -26,87 +26,116 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Perform diffusion image pre-processing using FSL\'s eddy tool; ' 'including inhomogeneity distortion correction using FSL\'s topup tool if possible') - cmdline.add_description('This script is intended to provide convenience of use of the FSL software tools topup and eddy for performing DWI pre-processing, ' - 'by encapsulating some of the surrounding image data and metadata processing steps. ' - 'It is intended to simply these processing steps for most commonly-used DWI acquisition strategies, ' - 'whilst also providing support for some more exotic acquisitions. ' - 'The "example usage" section demonstrates the ways in which the script can be used based on the (compulsory) -rpe_* command-line options.') + cmdline.add_description('This script is intended to provide convenience of use of the FSL software tools' + ' topup and eddy for performing DWI pre-processing,' + ' by encapsulating some of the surrounding image data and metadata processing steps.' + ' It is intended to simply these processing steps for most commonly-used DWI acquisition strategies,' + ' whilst also providing support for some more exotic acquisitions.' + ' The "example usage" section demonstrates the ways in which the script can be used' + ' based on the (compulsory) -rpe_* command-line options.') cmdline.add_description('More information on use of the dwifslpreproc command can be found at the following link: \n' f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/dwifslpreproc.html') - cmdline.add_description('Note that the MRtrix3 command dwi2mask will automatically be called to derive a processing mask for the FSL command "eddy", ' - 'which determines which voxels contribute to the estimation of geometric distortion parameters and possibly also the classification of outlier slices. ' - 'If FSL command "topup" is used to estimate a susceptibility field, ' - 'then dwi2mask will be executed on the resuts of running FSL command "applytopup" to the input DWIs; ' - 'otherwise it will be executed directly on the input DWIs. ' - 'Alternatively, the -eddy_mask option can be specified in order to manually provide such a processing mask. ' - 'More information on mask derivation from DWI data can be found at: ' + cmdline.add_description('Note that the MRtrix3 command dwi2mask will automatically be called' + ' to derive a processing mask for the FSL command "eddy",' + ' which determines which voxels contribute to the estimation of geometric distortion parameters' + ' and possibly also the classification of outlier slices.' + ' If FSL command "topup" is used to estimate a susceptibility field,' + ' then dwi2mask will be executed on the resuts of running FSL command "applytopup" to the input DWIs;' + ' otherwise it will be executed directly on the input DWIs.' + ' Alternatively, the -eddy_mask option can be specified in order to manually provide such a processing mask.' + ' More information on mask derivation from DWI data can be found at: \n' f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') - cmdline.add_description('The "-topup_options" and "-eddy_options" command-line options allow the user to pass desired command-line options directly to the FSL commands topup and eddy. ' - 'The available options for those commands may vary between versions of FSL; ' - 'users can interrogate such by querying the help pages of the installed software, ' - 'and/or the FSL online documentation: ' - '(topup) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide ; ' + cmdline.add_description('The "-topup_options" and "-eddy_options" command-line options allow the user' + ' to pass desired command-line options directly to the FSL commands topup and eddy.' + ' The available options for those commands may vary between versions of FSL;' + ' users can interrogate such by querying the help pages of the installed software,' + ' and/or the FSL online documentation: \n' + '(topup) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup/TopupUsersGuide ; \n' '(eddy) https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide') - cmdline.add_description('The script will attempt to run the CUDA version of eddy; ' - 'if this does not succeed for any reason, or is not present on the system, ' - 'the CPU version will be attempted instead. ' - 'By default, the CUDA eddy binary found that indicates compilation against the most recent version of CUDA will be attempted; ' - 'this can be over-ridden by providing a soft-link "eddy_cuda" within your path that links to the binary you wish to be executed.') - cmdline.add_description('Note that this script does not perform any explicit registration between images provided to topup via the -se_epi option, ' - 'and the DWI volumes provided to eddy. ' - 'In some instances (motion between acquisitions) this can result in erroneous application of the inhomogeneity field during distortion correction. ' - 'Use of the -align_seepi option is advocated in this scenario, ' - 'which ensures that the first volume in the series provided to topup is also the first volume in the series provided to eddy, ' - 'guaranteeing alignment. ' - 'But a prerequisite for this approach is that the image contrast within the images provided to the -se_epi option ' - 'must match the b=0 volumes present within the input DWI series: ' - 'this means equivalent TE, TR and flip angle ' - '(note that differences in multi-band factors between two acquisitions may lead to differences in TR).') - cmdline.add_example_usage('A basic DWI acquisition, where all image volumes are acquired in a single protocol with fixed phase encoding', + cmdline.add_description('The script will attempt to run the CUDA version of eddy;' + ' if this does not succeed for any reason,' + ' or is not present on the system,' + ' the CPU version will be attempted instead.' + ' By default, the CUDA eddy binary found that indicates compilation' + ' against the most recent version of CUDA will be attempted;' + ' this can be over-ridden by providing a soft-link "eddy_cuda" within your path' + ' that links to the binary you wish to be executed.') + cmdline.add_description('Note that this script does not perform any explicit registration' + ' between images provided to topup via the -se_epi option,' + ' and the DWI volumes provided to eddy.' + ' In some instances (motion between acquisitions)' + ' this can result in erroneous application of the inhomogeneity field during distortion correction.' + ' Use of the -align_seepi option is advocated in this scenario,' + ' which ensures that the first volume in the series provided to topup' + ' is also the first volume in the series provided to eddy,' + ' guaranteeing alignment.' + ' But a prerequisite for this approach is that the image contrast' + ' within the images provided to the -se_epi option' + ' must match the b=0 volumes present within the input DWI series:' + ' this means equivalent TE, TR and flip angle' + ' (note that differences in multi-band factors between two acquisitions' + ' may lead to differences in TR).') + cmdline.add_example_usage('A basic DWI acquisition,' + ' where all image volumes are acquired in a single protocol with fixed phase encoding', 'dwifslpreproc DWI_in.mif DWI_out.mif -rpe_none -pe_dir ap -readout_time 0.55', - 'Due to use of a single fixed phase encoding, no EPI distortion correction can be applied in this case.') + 'Due to use of a single fixed phase encoding,' + ' no EPI distortion correction can be applied in this case.') cmdline.add_example_usage('DWIs all acquired with a single fixed phase encoding; ' - 'but additionally a pair of b=0 images with reversed phase encoding to estimate the inhomogeneity field', - 'mrcat b0_ap.mif b0_pa.mif b0_pair.mif -axis 3; ' - 'dwifslpreproc DWI_in.mif DWI_out.mif -rpe_pair -se_epi b0_pair.mif -pe_dir ap -readout_time 0.72 -align_seepi', - 'Here the two individual b=0 volumes are concatenated into a single 4D image series, ' - 'and this is provided to the script via the -se_epi option. ' - 'Note that with the -rpe_pair option used here, ' - 'which indicates that the SE-EPI image series contains one or more pairs of b=0 images with reversed phase encoding, ' - 'the FIRST HALF of the volumes in the SE-EPI series must possess the same phase encoding as the input DWI series, ' - 'while the second half are assumed to contain the opposite phase encoding direction but identical total readout time. ' - 'Use of the -align_seepi option is advocated as long as its use is valid ' - '(more information in the Description section).') - cmdline.add_example_usage('All DWI directions & b-values are acquired twice, ' - 'with the phase encoding direction of the second acquisition protocol being reversed with respect to the first', - 'mrcat DWI_lr.mif DWI_rl.mif DWI_all.mif -axis 3; ' - 'dwifslpreproc DWI_all.mif DWI_out.mif -rpe_all -pe_dir lr -readout_time 0.66', - 'Here the two acquisition protocols are concatenated into a single DWI series containing all acquired volumes. ' - 'The direction indicated via the -pe_dir option should be the direction of ' - 'phase encoding used in acquisition of the FIRST HALF of volumes in the input DWI series; ' - 'ie. the first of the two files that was provided to the mrcat command. ' - 'In this usage scenario, ' - 'the output DWI series will contain the same number of image volumes as ONE of the acquired DWI series ' - '(ie. half of the number in the concatenated series); ' - 'this is because the script will identify pairs of volumes that possess the same diffusion sensitisation but reversed phase encoding, ' - 'and perform explicit recombination of those volume pairs in such a way that image contrast in ' - 'regions of inhomogeneity is determined from the stretched rather than the compressed image.') + 'but additionally a pair of b=0 images with reversed phase encoding' + ' to estimate the inhomogeneity field', + 'mrcat b0_ap.mif b0_pa.mif b0_pair.mif -axis 3;' + ' dwifslpreproc DWI_in.mif DWI_out.mif -rpe_pair -se_epi b0_pair.mif -pe_dir ap -readout_time 0.72 -align_seepi', + 'Here the two individual b=0 volumes are concatenated into a single 4D image series,' + ' and this is provided to the script via the -se_epi option.' + ' Note that with the -rpe_pair option used here,' + ' which indicates that the SE-EPI image series contains' + ' one or more pairs of b=0 images with reversed phase encoding,' + ' the FIRST HALF of the volumes in the SE-EPI series must possess' + ' the same phase encoding as the input DWI series,' + ' while the second half are assumed to contain the opposite' + ' phase encoding direction but identical total readout time.' + ' Use of the -align_seepi option is advocated as long as its use is valid' + ' (more information in the Description section).') + cmdline.add_example_usage('All DWI directions & b-values are acquired twice,' + ' with the phase encoding direction of the second acquisition protocol' + ' being reversed with respect to the first', + 'mrcat DWI_lr.mif DWI_rl.mif DWI_all.mif -axis 3;' + ' dwifslpreproc DWI_all.mif DWI_out.mif -rpe_all -pe_dir lr -readout_time 0.66', + 'Here the two acquisition protocols are concatenated' + ' into a single DWI series containing all acquired volumes.' + ' The direction indicated via the -pe_dir option should be the direction of' + ' phase encoding used in acquisition of the FIRST HALF of volumes in the input DWI series;' + ' ie. the first of the two files that was provided to the mrcat command.' + ' In this usage scenario,' + ' the output DWI series will contain the same number of image volumes' + ' as ONE of the acquired DWI series' + ' (ie. half of the number in the concatenated series);' + ' this is because the script will identify pairs of volumes that possess' + ' the same diffusion sensitisation but reversed phase encoding,' + ' and perform explicit recombination of those volume pairs in such a way' + ' that image contrast in regions of inhomogeneity is determined' + ' from the stretched rather than the compressed image.') cmdline.add_example_usage('Any acquisition scheme that does not fall into one of the example usages above', - 'mrcat DWI_*.mif DWI_all.mif -axis 3; ' - 'mrcat b0_*.mif b0_all.mif -axis 3; ' - 'dwifslpreproc DWI_all.mif DWI_out.mif -rpe_header -se_epi b0_all.mif -align_seepi', - 'With this usage, ' - 'the relevant phase encoding information is determined entirely based on the contents of the relevant image headers, ' - 'and dwifslpreproc prepares all metadata for the executed FSL commands accordingly. ' - 'This can therefore be used if the particular DWI acquisition strategy used does not correspond to one of the simple examples as described in the prior examples. ' - 'This usage is predicated on the headers of the input files containing appropriately-named key-value fields such that MRtrix3 tools identify them as such. ' - 'In some cases, conversion from DICOM using MRtrix3 commands will automatically extract and embed this information; ' - 'however this is not true for all scanner vendors and/or software versions. ' - 'In the latter case it may be possible to manually provide these metadata; ' - 'either using the -json_import command-line option of dwifslpreproc, ' - 'or the -json_import or one of the -import_pe_* command-line options of MRtrix3\'s mrconvert command ' - '(and saving in .mif format) ' - 'prior to running dwifslpreproc.') + 'mrcat DWI_*.mif DWI_all.mif -axis 3;' + ' mrcat b0_*.mif b0_all.mif -axis 3;' + ' dwifslpreproc DWI_all.mif DWI_out.mif -rpe_header -se_epi b0_all.mif -align_seepi', + 'With this usage,' + ' the relevant phase encoding information is determined' + ' entirely based on the contents of the relevant image headers,' + ' and dwifslpreproc prepares all metadata for the executed FSL commands accordingly. ' + ' This can therefore be used if the particular DWI acquisition strategy used' + ' does not correspond to one of the simple examples as described in the prior examples.' + ' This usage is predicated on the headers of the input files' + ' containing appropriately-named key-value fields' + ' such that MRtrix3 tools identify them as such.' + ' In some cases,' + ' conversion from DICOM using MRtrix3 commands will automatically extract and embed this information;' + ' however this is not true for all scanner vendors and/or software versions.' + ' In the latter case it may be possible to manually provide these metadata;' + ' either using the -json_import command-line option of dwifslpreproc,' + ' or the -json_import or one of the -import_pe_* command-line options of MRtrix3\'s mrconvert command' + ' (and saving in .mif format)' + ' prior to running dwifslpreproc.') cmdline.add_citation('Andersson, J. L. & Sotiropoulos, S. N. ' 'An integrated approach to correction for off-resonance effects and subject movement in diffusion MR imaging. ' 'NeuroImage, 2015, 125, 1063-1078', @@ -118,7 +147,8 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_citation('Skare, S. & Bammer, R. ' 'Jacobian weighting of distortion corrected EPI data. ' 'Proceedings of the International Society for Magnetic Resonance in Medicine, 2010, 5063', - condition='If performing recombination of diffusion-weighted volume pairs with opposing phase encoding directions', + condition='If performing recombination of diffusion-weighted volume pairs' + ' with opposing phase encoding directions', is_external=True) cmdline.add_citation('Andersson, J. L.; Skare, S. & Ashburner, J. ' 'How to correct susceptibility distortions in spin-echo echo-planar images: ' @@ -151,15 +181,15 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_argument('-json_import', type=app.Parser.FileIn(), metavar='file', - help='Import image header information from an associated JSON file ' - '(may be necessary to determine phase encoding information)') + help='Import image header information from an associated JSON file' + ' (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') pe_options.add_argument('-pe_dir', metavar='PE', - help='Manually specify the phase encoding direction of the input series; ' - 'can be a signed axis number (e.g. -0, 1, +2), ' - 'an axis designator (e.g. RL, PA, IS), ' - 'or NIfTI axis codes (e.g. i-, j, k)') + help='Manually specify the phase encoding direction of the input series;' + ' can be a signed axis number (e.g. -0, 1, +2),' + ' an axis designator (e.g. RL, PA, IS),' + ' or NIfTI axis codes (e.g. i-, j, k)') pe_options.add_argument('-readout_time', type=app.Parser.Float(0.0), metavar='time', @@ -168,18 +198,18 @@ def usage(cmdline): #pylint: disable=unused-variable distcorr_options.add_argument('-se_epi', type=app.Parser.ImageIn(), metavar='image', - help='Provide an additional image series consisting of spin-echo EPI images, ' - 'which is to be used exclusively by topup for estimating the inhomogeneity field ' - '(i.e. it will not form part of the output image series)') + help='Provide an additional image series consisting of spin-echo EPI images,' + ' which is to be used exclusively by topup for estimating the inhomogeneity field' + ' (i.e. it will not form part of the output image series)') distcorr_options.add_argument('-align_seepi', action='store_true', - help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation and the DWIs ' - '(more information in Description section)') + help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation and the DWIs' + ' (more information in Description section)') distcorr_options.add_argument('-topup_options', metavar='" TopupOptions"', - help='Manually provide additional command-line options to the topup command ' - '(provide a string within quotation marks that contains at least one space, ' - 'even if only passing a single command-line option to topup)') + help='Manually provide additional command-line options to the topup command' + ' (provide a string within quotation marks that contains at least one space,' + ' even if only passing a single command-line option to topup)') distcorr_options.add_argument('-topup_files', metavar='prefix', help='Provide files generated by prior execution of the FSL "topup" command to be utilised by eddy') @@ -190,50 +220,52 @@ def usage(cmdline): #pylint: disable=unused-variable eddy_options.add_argument('-eddy_mask', type=app.Parser.ImageIn(), metavar='image', - help='Provide a processing mask to use for eddy, ' - 'instead of having dwifslpreproc generate one internally using dwi2mask') + help='Provide a processing mask to use for eddy,' + ' instead of having dwifslpreproc generate one internally using dwi2mask') eddy_options.add_argument('-eddy_slspec', type=app.Parser.FileIn(), metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') eddy_options.add_argument('-eddy_options', metavar='" EddyOptions"', - help='Manually provide additional command-line options to the eddy command ' - '(provide a string within quotation marks that contains at least one space, ' - 'even if only passing a single command-line option to eddy)') + help='Manually provide additional command-line options to the eddy command' + ' (provide a string within quotation marks that contains at least one space,' + ' even if only passing a single command-line option to eddy)') eddyqc_options = cmdline.add_argument_group('Options for utilising EddyQC') eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.DirectoryOut(), metavar='directory', - help='Copy the various text-based statistical outputs generated by eddy, ' - 'and the output of eddy_qc (if installed), ' - 'into an output directory') + help='Copy the various text-based statistical outputs generated by eddy,' + ' and the output of eddy_qc (if installed),' + ' into an output directory') eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.DirectoryOut(), metavar='directory', - help='Copy ALL outputs generated by eddy (including images), ' - 'and the output of eddy_qc (if installed), ' - 'into an output directory') + help='Copy ALL outputs generated by eddy (including images),' + ' and the output of eddy_qc (if installed),' + ' into an output directory') cmdline.flag_mutually_exclusive_options( [ 'eddyqc_text', 'eddyqc_all' ], False ) app.add_dwgrad_export_options(cmdline) app.add_dwgrad_import_options(cmdline) - rpe_options = cmdline.add_argument_group('Options for specifying the acquisition phase-encoding design; ' - 'note that one of the -rpe_* options MUST be provided') + rpe_options = cmdline.add_argument_group('Options for specifying the acquisition phase-encoding design;' + ' note that one of the -rpe_* options MUST be provided') rpe_options.add_argument('-rpe_none', action='store_true', - help='Specify that no reversed phase-encoding image data is being provided; ' - 'eddy will perform eddy current and motion correction only') + help='Specify that no reversed phase-encoding image data is being provided;' + ' eddy will perform eddy current and motion correction only') rpe_options.add_argument('-rpe_pair', action='store_true', - help='Specify that a set of images (typically b=0 volumes) will be provided for use in inhomogeneity field estimation only ' - '(using the -se_epi option)') + help='Specify that a set of images' + ' (typically b=0 volumes)' + ' will be provided for use in inhomogeneity field estimation only' + ' (using the -se_epi option)') rpe_options.add_argument('-rpe_all', action='store_true', help='Specify that ALL DWIs have been acquired with opposing phase-encoding') rpe_options.add_argument('-rpe_header', action='store_true', - help='Specify that the phase-encoding information can be found in the image header(s), ' - 'and that this is the information that the script should use') + help='Specify that the phase-encoding information can be found in the image header(s),' + ' and that this is the information that the script should use') cmdline.flag_mutually_exclusive_options( [ 'rpe_none', 'rpe_pair', 'rpe_all', 'rpe_header' ], True ) cmdline.flag_mutually_exclusive_options( [ 'rpe_none', 'se_epi' ], False ) # May still technically provide -se_epi even with -rpe_all cmdline.flag_mutually_exclusive_options( [ 'rpe_pair', 'topup_files'] ) # Would involve two separate sources of inhomogeneity field information diff --git a/python/bin/dwigradcheck b/python/bin/dwigradcheck index 5b39ea2288..611530c62a 100755 --- a/python/bin/dwigradcheck +++ b/python/bin/dwigradcheck @@ -24,9 +24,11 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Check the orientation of the diffusion gradient table') cmdline.add_description('Note that the corrected gradient table can be output using the -export_grad_{mrtrix,fsl} option.') - cmdline.add_description('Note that if the -mask command-line option is not specified, the MRtrix3 command dwi2mask will automatically be called to ' - 'derive a binary mask image to be used for streamline seeding and to constrain streamline propagation. ' - 'More information on mask derivation from DWI data can be found at the following link: \n' + cmdline.add_description('Note that if the -mask command-line option is not specified,' + ' the MRtrix3 command dwi2mask will automatically be called' + ' to derive a binary mask image to be used for streamline seeding' + ' and to constrain streamline propagation.' + ' More information on mask derivation from DWI data can be found at the following link: \n' f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/dwi_preprocessing/masking.html') cmdline.add_citation('Jeurissen, B.; Leemans, A.; Sijbers, J. ' 'Automated correction of improperly rotated diffusion gradient orientations in diffusion weighted MRI. ' diff --git a/python/bin/dwinormalise b/python/bin/dwinormalise index 6f5ee6d49c..db6ea2720f 100755 --- a/python/bin/dwinormalise +++ b/python/bin/dwinormalise @@ -20,11 +20,14 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import algorithm #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Perform various forms of intensity normalisation of DWIs') - cmdline.add_description('This script provides access to different techniques for globally scaling the intensity of diffusion-weighted images. ' - 'The different algorithms have different purposes, ' - 'and different requirements with respect to the data with which they must be provided & will produce as output. ' - 'Further information on the individual algorithms available can be accessed via their individual help pages; ' - 'eg. "dwinormalise group -help".') + cmdline.add_description('This script provides access to different techniques' + ' for globally scaling the intensity of diffusion-weighted images.' + ' The different algorithms have different purposes,' + ' and different requirements with respect to the data' + ' with which they must be provided & will produce as output.' + ' Further information on the individual algorithms available' + ' can be accessed via their individual help pages;' + ' eg. "dwinormalise group -help".') # Import the command-line settings for all algorithms found in the relevant directory algorithm.usage(cmdline) diff --git a/python/bin/dwishellmath b/python/bin/dwishellmath index 9d1a192e65..5b06cecc37 100755 --- a/python/bin/dwishellmath +++ b/python/bin/dwishellmath @@ -23,18 +23,18 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Daan Christiaens (daan.christiaens@kcl.ac.uk)') cmdline.set_synopsis('Apply an mrmath operation to each b-value shell in a DWI series') - cmdline.add_description('The output of this command is a 4D image, ' - 'where each volume corresponds to a b-value shell ' - '(in order of increasing b-value), ' - 'an the intensities within each volume correspond to the chosen statistic having been ' - 'computed from across the DWI volumes belonging to that b-value shell.') + cmdline.add_description('The output of this command is a 4D image,' + ' where each volume corresponds to a b-value shell' + ' (in order of increasing b-value),' + ' and the intensities within each volume correspond to the chosen statistic' + ' having been computed from across the DWI volumes belonging to that b-value shell.') cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input diffusion MRI series') cmdline.add_argument('operation', choices=SUPPORTED_OPS, - help='The operation to be applied to each shell; ' - 'this must be one of the following: ' + help='The operation to be applied to each shell;' + ' this must be one of the following: ' + ', '.join(SUPPORTED_OPS)) cmdline.add_argument('output', type=app.Parser.ImageOut(), diff --git a/python/bin/for_each b/python/bin/for_each index 1743bfd760..0931606b8b 100755 --- a/python/bin/for_each +++ b/python/bin/for_each @@ -31,19 +31,24 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import _version #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) and David Raffelt (david.raffelt@florey.edu.au)') cmdline.set_synopsis('Perform some arbitrary processing step for each of a set of inputs') - cmdline.add_description('This script greatly simplifies various forms of batch processing by enabling the execution of a command ' - '(or set of commands) ' - 'independently for each of a set of inputs.') + cmdline.add_description('This script greatly simplifies various forms of batch processing' + ' by enabling the execution of a command' + ' (or set of commands)' + ' independently for each of a set of inputs.') cmdline.add_description('More information on use of the for_each command can be found at the following link: \n' f'https://mrtrix.readthedocs.io/en/{_version.__tag__}/tips_and_tricks/batch_processing_with_foreach.html') - cmdline.add_description('The way that this batch processing capability is achieved is by providing basic text substitutions, ' - 'which simplify the formation of valid command strings based on the unique components of the input strings on which the script is instructed to execute. ' - 'This does however mean that the items to be passed as inputs to the for_each command ' - '(e.g. file / directory names) ' - 'MUST NOT contain any instances of these substitution strings, ' - 'as otherwise those paths will be corrupted during the course of the substitution.') + cmdline.add_description('The way that this batch processing capability is achieved' + ' is by providing basic text substitutions,' + ' which simplify the formation of valid command strings' + ' based on the unique components of the input strings' + ' on which the script is instructed to execute. ' + 'This does however mean that the items to be passed as inputs to the for_each command' + ' (e.g. file / directory names)' + ' MUST NOT contain any instances of these substitution strings,' + ' as otherwise those paths will be corrupted during the course of the substitution.') cmdline.add_description('The available substitutions are listed below ' - '(note that the -test command-line option can be used to ensure correct command string formation prior to actually executing the commands):') + '(note that the -test command-line option can be used' + ' to ensure correct command string formation prior to actually executing the commands):') cmdline.add_description(' - IN: ' 'The full matching pattern, including leading folders. ' 'For example, if the target list contains a file "folder/image.mif", ' @@ -61,52 +66,61 @@ def usage(cmdline): #pylint: disable=unused-variable 'The unique part of the input after removing any common prefix and common suffix. ' 'For example, if the target list contains files: "folder/001dwi.mif", "folder/002dwi.mif", "folder/003dwi.mif", ' 'any occurrence of "UNI" will be substituted with "001", "002", "003".') - cmdline.add_description('Note that due to a limitation of the Python "argparse" module, ' - 'any command-line OPTIONS that the user intends to provide specifically to the for_each script ' - 'must appear BEFORE providing the list of inputs on which for_each is intended to operate. ' - 'While command-line options provided as such will be interpreted specifically by the for_each script, ' - 'any command-line options that are provided AFTER the COLON separator will form part of the executed COMMAND, ' - 'and will therefore be interpreted as command-line options having been provided to that underlying command.') + cmdline.add_description('Note that due to a limitation of the Python "argparse" module,' + ' any command-line OPTIONS that the user intends to provide specifically to the for_each script' + ' must appear BEFORE providing the list of inputs on which for_each is intended to operate.' + ' While command-line options provided as such will be interpreted specifically by the for_each script,' + ' any command-line options that are provided AFTER the COLON separator will form part of the executed COMMAND,' + ' and will therefore be interpreted as command-line options having been provided to that underlying command.') cmdline.add_example_usage('Demonstration of basic usage syntax', 'for_each folder/*.mif : mrinfo IN', - 'This will run the "mrinfo" command for every .mif file present in "folder/". ' - 'Note that the compulsory colon symbol is used to separate the list of items on which for_each is being instructed to operate, ' - 'from the command that is intended to be run for each input.') + 'This will run the "mrinfo" command for every .mif file present in "folder/".' + ' Note that the compulsory colon symbol is used' + ' to separate the list of items on which for_each is being instructed to operate' + ' from the command that is intended to be run for each input.') cmdline.add_example_usage('Multi-threaded use of for_each', 'for_each -nthreads 4 freesurfer/subjects/* : recon-all -subjid NAME -all', 'In this example, ' - 'for_each is instructed to run the FreeSurfer command "recon-all" for all subjects within the "subjects" directory, ' - 'with four subjects being processed in parallel at any one time. ' - 'Whenever processing of one subject is completed, ' - 'processing for a new unprocessed subject will commence. ' - 'This technique is useful for improving the efficiency of running single-threaded commands on multi-core systems, ' - 'as long as the system possesses enough memory to support such parallel processing. ' - 'Note that in the case of multi-threaded commands ' - '(which includes many MRtrix3 commands), ' - 'it is generally preferable to permit multi-threaded execution of the command on a single input at a time, ' - 'rather than processing multiple inputs in parallel.') + 'for_each is instructed to run the FreeSurfer command "recon-all"' + ' for all subjects within the "subjects" directory,' + ' with four subjects being processed in parallel at any one time.' + ' Whenever processing of one subject is completed,' + ' processing for a new unprocessed subject will commence.' + ' This technique is useful for improving the efficiency' + ' of running single-threaded commands on multi-core systems,' + ' as long as the system possesses enough memory to support such parallel processing.' + ' Note that in the case of multi-threaded commands' + ' (which includes many MRtrix3 commands),' + ' it is generally preferable to permit multi-threaded execution' + ' of the command on a single input at a time,' + ' rather than processing multiple inputs in parallel.') cmdline.add_example_usage('Excluding specific inputs from execution', 'for_each *.nii -exclude 001.nii : mrconvert IN PRE.mif', - 'Particularly when a wildcard is used to define the list of inputs for for_each, ' - 'it is possible in some instances that this list will include one or more strings for which execution should in fact not be performed; ' - 'for instance, if a command has already been executed for one or more files, ' - 'and then for_each is being used to execute the same command for all other files. ' - 'In this case, ' - 'the -exclude option can be used to effectively remove an item from the list of inputs that would otherwise be included due to the use of a wildcard ' - '(and can be used more than once to exclude more than one string). ' - 'In this particular example, ' - 'mrconvert is instructed to perform conversions from NIfTI to MRtrix image formats, ' - 'for all except the first image in the directory. ' - 'Note that any usages of this option must appear AFTER the list of inputs. ' - 'Note also that the argument following the -exclude option can alternatively be a regular expression, ' - 'in which case any inputs for which a match to the expression is found will be excluded from processing.') + 'Particularly when a wildcard is used to define the list of inputs for for_each,' + ' it is possible in some instances that this list will include one or more strings' + ' for which execution should in fact not be performed;' + ' for instance, if a command has already been executed for one or more files,' + ' and then for_each is being used to execute the same command for all other files.' + ' In this case,' + ' the -exclude option can be used to effectively remove an item' + ' from the list of inputs that would otherwise be included due to the use of a wildcard' + ' (and can be used more than once to exclude more than one string).' + ' In this particular example,' + ' mrconvert is instructed to perform conversions from NIfTI to MRtrix image formats,' + ' for all except the first image in the directory.' + ' Note that any usages of this option must appear AFTER the list of inputs.' + ' Note also that the argument following the -exclude option' + ' can alternatively be a regular expression,' + ' in which case any inputs for which a match to the expression is found' + ' will be excluded from processing.') cmdline.add_example_usage('Testing the command string substitution', 'for_each -test * : mrconvert IN PRE.mif', - 'By specifying the -test option, ' - 'the script will print to the terminal the results of text substitutions for all of the specified inputs, ' - 'but will not actually execute those commands. ' - 'It can therefore be used to verify that the script is receiving the intended set of inputs, ' - 'and that the text substitutions on those inputs lead to the intended command strings.') + 'By specifying the -test option,' + ' the script will print to the terminal the results of text substitutions' + ' for all of the specified inputs,' + ' but will not actually execute those commands.' + ' It can therefore be used to verify that the script is receiving the intended set of inputs,' + ' and that the text substitutions on those inputs lead to the intended command strings.') cmdline.add_argument('inputs', nargs='+', help='Each of the inputs for which processing should be run') @@ -127,8 +141,8 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_argument('-test', action='store_true', default=False, - help='Test the operation of the for_each script, ' - 'by printing the command strings following string substitution but not actually executing them') + help='Test the operation of the for_each script,' + ' by printing the command strings following string substitution but not actually executing them') # Usage of for_each needs to be handled slightly differently here: # We want argparse to parse only the contents of the command-line before the colon symbol, diff --git a/python/bin/labelsgmfix b/python/bin/labelsgmfix index e1b941a440..ad8d7de722 100755 --- a/python/bin/labelsgmfix +++ b/python/bin/labelsgmfix @@ -32,8 +32,8 @@ import math, os def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - cmdline.set_synopsis('In a FreeSurfer parcellation image, ' - 'replace the sub-cortical grey matter structure delineations using FSL FIRST') + cmdline.set_synopsis('In a FreeSurfer parcellation image,' + ' replace the sub-cortical grey matter structure delineations using FSL FIRST') cmdline.add_citation('Patenaude, B.; Smith, S. M.; Kennedy, D. N. & Jenkinson, M. ' 'A Bayesian model of shape and appearance for subcortical brain segmentation. ' 'NeuroImage, 2011, 56, 907-922', @@ -64,8 +64,8 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_argument('-sgm_amyg_hipp', action='store_true', default=False, - help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures, ' - 'and also replace their estimates with those from FIRST') + help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures,' + ' and also replace their estimates with those from FIRST') diff --git a/python/bin/mask2glass b/python/bin/mask2glass index 3a3e028450..d2c8fab50b 100755 --- a/python/bin/mask2glass +++ b/python/bin/mask2glass @@ -17,16 +17,18 @@ def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Remika Mito (remika.mito@florey.edu.au) ' - 'and Robert E. Smith (robert.smith@florey.edu.au)') + cmdline.set_author('Remika Mito (remika.mito@florey.edu.au)' + ' and Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Create a glass brain from mask input') - cmdline.add_description('The output of this command is a glass brain image, ' - 'which can be viewed using the volume render option in mrview, ' - 'and used for visualisation purposes to view results in 3D.') - cmdline.add_description('While the name of this script indicates that a binary mask image is required as input, ' - 'it can also operate on a floating-point image. ' - 'One way in which this can be exploited is to compute the mean of all subject masks within template space, ' - 'in which case this script will produce a smoother result than if a binary template mask were to be used as input.') + cmdline.add_description('The output of this command is a glass brain image,' + ' which can be viewed using the volume render option in mrview,' + ' and used for visualisation purposes to view results in 3D.') + cmdline.add_description('While the name of this script indicates that a binary mask image is required as input,' + ' it can also operate on a floating-point image.' + ' One way in which this can be exploited' + ' is to compute the mean of all subject masks within template space,' + ' in which case this script will produce a smoother result' + ' than if a binary template mask were to be used as input.') cmdline.add_argument('input', type=app.Parser.ImageIn(), help='The input mask image') @@ -36,15 +38,18 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.add_argument('-dilate', type=app.Parser.Int(0), default=2, - help='Provide number of passes for dilation step; default = 2') + help='Provide number of passes for dilation step;' + ' default = 2') cmdline.add_argument('-scale', type=app.Parser.Float(0.0), default=2.0, - help='Provide resolution upscaling value; default = 2.0') + help='Provide resolution upscaling value;' + ' default = 2.0') cmdline.add_argument('-smooth', type=app.Parser.Float(0.0), default=1.0, - help='Provide standard deviation of smoothing (in mm); default = 1.0') + help='Provide standard deviation of smoothing (in mm);' + ' default = 1.0') def execute(): #pylint: disable=unused-variable diff --git a/python/bin/mrtrix_cleanup b/python/bin/mrtrix_cleanup index a2a7702226..f1275414c1 100755 --- a/python/bin/mrtrix_cleanup +++ b/python/bin/mrtrix_cleanup @@ -27,24 +27,28 @@ def usage(cmdline): #pylint: disable=unused-variable cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') cmdline.set_synopsis('Clean up residual temporary files & scratch directories from MRtrix3 commands') - cmdline.add_description('This script will search the file system at the specified location ' - '(and in sub-directories thereof) ' - 'for any temporary files or directories that have been left behind by failed or terminated MRtrix3 commands, ' - 'and attempt to delete them.') - cmdline.add_description('Note that the script\'s search for temporary items will not extend beyond the user-specified filesystem location. ' - 'This means that any built-in or user-specified default location for MRtrix3 piped data and scripts will not be automatically searched. ' - 'Cleanup of such locations should instead be performed explicitly: ' - 'e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') - cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed: ' - 'it may delete temporary items during operation that may lead to unexpected behaviour.') + cmdline.add_description('This script will search the file system at the specified location' + ' (and in sub-directories thereof)' + ' for any temporary files or directories that have been left behind' + ' by failed or terminated MRtrix3 commands,' + ' and attempt to delete them.') + cmdline.add_description('Note that the script\'s search for temporary items' + ' will not extend beyond the user-specified filesystem location.' + ' This means that any built-in or user-specified default location' + ' for MRtrix3 piped data and scripts will not be automatically searched.' + ' Cleanup of such locations should instead be performed explicitly:' + ' e.g. "mrtrix_cleanup /tmp/" to remove residual piped images from /tmp/.') + cmdline.add_description('This script should not be run while other MRtrix3 commands are being executed:' + ' it may delete temporary items during operation' + ' that may lead to unexpected behaviour.') cmdline.add_argument('path', type=app.Parser.DirectoryIn(), help='Directory from which to commence filesystem search') cmdline.add_argument('-test', action='store_true', - help='Run script in test mode: ' - 'will list identified files / directories, ' - 'but not attempt to delete them') + help='Run script in test mode:' + ' will list identified files / directories,' + ' but not attempt to delete them') cmdline.add_argument('-failed', type=app.Parser.FileOut(), metavar='file', diff --git a/python/bin/population_template b/python/bin/population_template index 70d3e9e39f..d6219ea94a 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -34,7 +34,13 @@ DEFAULT_NL_UPDATE_SMOOTH = 2.0 DEFAULT_NL_DISP_SMOOTH = 1.0 DEFAULT_NL_GRAD_STEP = 0.5 -REGISTRATION_MODES = ['rigid', 'affine', 'nonlinear', 'rigid_affine', 'rigid_nonlinear', 'affine_nonlinear', 'rigid_affine_nonlinear'] +REGISTRATION_MODES = ['rigid', + 'affine', + 'nonlinear', + 'rigid_affine', + 'rigid_nonlinear', + 'affine_nonlinear', + 'rigid_affine_nonlinear'] AGGREGATION_MODES = ['mean', 'median'] @@ -48,14 +54,14 @@ IMAGEEXT = ['mif', 'nii', 'mih', 'mgh', 'mgz', 'img', 'hdr'] def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('David Raffelt (david.raffelt@florey.edu.au) ' - '& Max Pietsch (maximilian.pietsch@kcl.ac.uk) ' - '& Thijs Dhollander (thijs.dhollander@gmail.com)') + cmdline.set_author('David Raffelt (david.raffelt@florey.edu.au)' + ' and Max Pietsch (maximilian.pietsch@kcl.ac.uk)' + ' and Thijs Dhollander (thijs.dhollander@gmail.com)') cmdline.set_synopsis('Generates an unbiased group-average template from a series of images') - cmdline.add_description('First a template is optimised with linear registration ' - '(rigid and/or affine, both by default), ' - 'then non-linear registration is used to optimise the template further.') + cmdline.add_description('First a template is optimised with linear registration' + ' (rigid and/or affine, both by default),' + ' then non-linear registration is used to optimise the template further.') cmdline.add_argument('input_dir', nargs='+', type=app.Parser.Various(), @@ -65,28 +71,36 @@ def usage(cmdline): #pylint: disable=unused-variable help='Output template image') cmdline.add_example_usage('Multi-contrast registration', 'population_template input_WM_ODFs/ output_WM_template.mif input_GM_ODFs/ output_GM_template.mif', - 'When performing multi-contrast registration, ' - 'the input directory and corresponding output template ' - 'image for a given contrast are to be provided as a pair, ' - 'with the pairs corresponding to different contrasts provided sequentially.') + 'When performing multi-contrast registration,' + ' the input directory and corresponding output template image' + ' for a given contrast are to be provided as a pair,' + ' with the pairs corresponding to different contrasts provided sequentially.') options = cmdline.add_argument_group('Multi-contrast options') options.add_argument('-mc_weight_initial_alignment', type=app.Parser.SequenceFloat(), - help='Weight contribution of each contrast to the initial alignment. ' - 'Comma separated, default: 1.0 for each contrast (ie. equal weighting).') + metavar='values', + help='Weight contribution of each contrast to the initial alignment.' + ' Comma separated,' + ' default: 1.0 for each contrast (ie. equal weighting).') options.add_argument('-mc_weight_rigid', type=app.Parser.SequenceFloat(), - help='Weight contribution of each contrast to the objective of rigid registration. ' - 'Comma separated, default: 1.0 for each contrast (ie. equal weighting)') + metavar='values', + help='Weight contribution of each contrast to the objective of rigid registration.' + ' Comma separated,' + ' default: 1.0 for each contrast (ie. equal weighting)') options.add_argument('-mc_weight_affine', type=app.Parser.SequenceFloat(), - help='Weight contribution of each contrast to the objective of affine registration. ' - 'Comma separated, default: 1.0 for each contrast (ie. equal weighting)') + metavar='values', + help='Weight contribution of each contrast to the objective of affine registration.' + ' Comma separated,' + ' default: 1.0 for each contrast (ie. equal weighting)') options.add_argument('-mc_weight_nl', type=app.Parser.SequenceFloat(), - help='Weight contribution of each contrast to the objective of nonlinear registration. ' - 'Comma separated, default: 1.0 for each contrast (ie. equal weighting)') + metavar='values', + help='Weight contribution of each contrast to the objective of nonlinear registration.' + ' Comma separated,' + ' default: 1.0 for each contrast (ie. equal weighting)') linoptions = cmdline.add_argument_group('Options for the linear registration') linoptions.add_argument('-linear_no_pause', @@ -97,170 +111,179 @@ def usage(cmdline): #pylint: disable=unused-variable help='Deactivate correction of template appearance (scale and shear) over iterations') linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, - help='Specify estimator for intensity difference metric. ' - 'Valid choices are: ' - 'l1 (least absolute: |x|), ' - 'l2 (ordinary least squares), ' - 'lp (least powers: |x|^1.2), ' - 'none (no robust estimator). ' - 'Default: none.') + help='Specify estimator for intensity difference metric.' + ' Valid choices are:' + ' l1 (least absolute: |x|),' + ' l2 (ordinary least squares),' + ' lp (least powers: |x|^1.2),' + ' none (no robust estimator).' + ' Default: none.') linoptions.add_argument('-rigid_scale', type=app.Parser.SequenceFloat(), - help='Specify the multi-resolution pyramid used to build the rigid template, ' - 'in the form of a list of scale factors ' - f'(default: {",".join([str(x) for x in DEFAULT_RIGID_SCALES])}). ' - 'This and affine_scale implicitly define the number of template levels') + help='Specify the multi-resolution pyramid used to build the rigid template,' + ' in the form of a list of scale factors' + f' (default: {",".join([str(x) for x in DEFAULT_RIGID_SCALES])}).' + ' This and affine_scale implicitly define the number of template levels') linoptions.add_argument('-rigid_lmax', type=app.Parser.SequenceInt(), - help='Specify the lmax used for rigid registration for each scale factor, ' - 'in the form of a list of integers ' - f'(default: {",".join([str(x) for x in DEFAULT_RIGID_LMAX])}). ' - 'The list must be the same length as the linear_scale factor list') + help='Specify the lmax used for rigid registration for each scale factor,' + ' in the form of a list of integers' + f' (default: {",".join([str(x) for x in DEFAULT_RIGID_LMAX])}).' + ' The list must be the same length as the linear_scale factor list') linoptions.add_argument('-rigid_niter', type=app.Parser.SequenceInt(), - help='Specify the number of registration iterations used within each level before updating the template, ' - 'in the form of a list of integers ' - '(default: 50 for each scale). ' - 'This must be a single number or a list of same length as the linear_scale factor list') + help='Specify the number of registration iterations used' + ' within each level before updating the template,' + ' in the form of a list of integers' + ' (default: 50 for each scale).' + ' This must be a single number' + ' or a list of same length as the linear_scale factor list') linoptions.add_argument('-affine_scale', type=app.Parser.SequenceFloat(), - help='Specify the multi-resolution pyramid used to build the affine template, ' - 'in the form of a list of scale factors ' - f'(default: {",".join([str(x) for x in DEFAULT_AFFINE_SCALES])}). ' - 'This and rigid_scale implicitly define the number of template levels') + help='Specify the multi-resolution pyramid used to build the affine template,' + ' in the form of a list of scale factors' + f' (default: {",".join([str(x) for x in DEFAULT_AFFINE_SCALES])}).' + ' This and rigid_scale implicitly define the number of template levels') linoptions.add_argument('-affine_lmax', type=app.Parser.SequenceInt(), - help='Specify the lmax used for affine registration for each scale factor, ' - 'in the form of a list of integers ' - f'(default: {",".join([str(x) for x in DEFAULT_AFFINE_LMAX])}). ' - 'The list must be the same length as the linear_scale factor list') + help='Specify the lmax used for affine registration for each scale factor,' + ' in the form of a list of integers' + f' (default: {",".join([str(x) for x in DEFAULT_AFFINE_LMAX])}).' + ' The list must be the same length as the linear_scale factor list') linoptions.add_argument('-affine_niter', type=app.Parser.SequenceInt(), - help='Specify the number of registration iterations used within each level before updating the template, ' - 'in the form of a list of integers ' - '(default: 500 for each scale). ' - 'This must be a single number or a list of same length as the linear_scale factor list') + help='Specify the number of registration iterations' + ' used within each level before updating the template,' + ' in the form of a list of integers' + ' (default: 500 for each scale).' + ' This must be a single number' + ' or a list of same length as the linear_scale factor list') nloptions = cmdline.add_argument_group('Options for the non-linear registration') nloptions.add_argument('-nl_scale', type=app.Parser.SequenceFloat(), - help='Specify the multi-resolution pyramid used to build the non-linear template, ' - 'in the form of a list of scale factors ' - f'(default: {" ".join([str(x) for x in DEFAULT_NL_SCALES])}). ' - 'This implicitly defines the number of template levels') + help='Specify the multi-resolution pyramid used to build the non-linear template,' + ' in the form of a list of scale factors' + f' (default: {",".join([str(x) for x in DEFAULT_NL_SCALES])}).' + ' This implicitly defines the number of template levels') nloptions.add_argument('-nl_lmax', type=app.Parser.SequenceInt(), - help='Specify the lmax used for non-linear registration for each scale factor, ' - 'in the form of a list of integers ' - f'(default: {",".join([str(x) for x in DEFAULT_NL_LMAX])}). ' - 'The list must be the same length as the nl_scale factor list') + help='Specify the lmax used for non-linear registration for each scale factor,' + ' in the form of a list of integers' + f' (default: {",".join([str(x) for x in DEFAULT_NL_LMAX])}).' + ' The list must be the same length as the nl_scale factor list') nloptions.add_argument('-nl_niter', type=app.Parser.SequenceInt(), - help='Specify the number of registration iterations used within each level before updating the template, ' - 'in the form of a list of integers ' - f'(default: {",".join([str(x) for x in DEFAULT_NL_NITER])}). ' - 'The list must be the same length as the nl_scale factor list') + help='Specify the number of registration iterations' + ' used within each level before updating the template,' + ' in the form of a list of integers' + f' (default: {",".join([str(x) for x in DEFAULT_NL_NITER])}).' + ' The list must be the same length as the nl_scale factor list') nloptions.add_argument('-nl_update_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_UPDATE_SMOOTH, - help='Regularise the gradient update field with Gaussian smoothing ' - '(standard deviation in voxel units, ' - f'Default {DEFAULT_NL_UPDATE_SMOOTH} x voxel_size)') + help='Regularise the gradient update field with Gaussian smoothing' + ' (standard deviation in voxel units,' + f' Default {DEFAULT_NL_UPDATE_SMOOTH} x voxel_size)') nloptions.add_argument('-nl_disp_smooth', type=app.Parser.Float(0.0), default=DEFAULT_NL_DISP_SMOOTH, - help='Regularise the displacement field with Gaussian smoothing ' - '(standard deviation in voxel units, ' - f'Default {DEFAULT_NL_DISP_SMOOTH} x voxel_size)') + help='Regularise the displacement field with Gaussian smoothing' + ' (standard deviation in voxel units,' + f' Default {DEFAULT_NL_DISP_SMOOTH} x voxel_size)') nloptions.add_argument('-nl_grad_step', type=app.Parser.Float(0.0), default=DEFAULT_NL_GRAD_STEP, - help='The gradient step size for non-linear registration ' - f'(Default: {DEFAULT_NL_GRAD_STEP})') + help='The gradient step size for non-linear registration' + f' (Default: {DEFAULT_NL_GRAD_STEP})') options = cmdline.add_argument_group('Input, output and general options') registration_modes_string = ', '.join(f'"{x}"' for x in REGISTRATION_MODES if '_' in x) options.add_argument('-type', choices=REGISTRATION_MODES, - help='Specify the types of registration stages to perform. ' - 'Options are: ' - '"rigid" (perform rigid registration only, ' - 'which might be useful for intra-subject registration in longitudinal analysis), ' - '"affine" (perform affine registration), ' - '"nonlinear", ' - f'as well as cominations of registration types: {registration_modes_string}. ' - 'Default: rigid_affine_nonlinear', + help='Specify the types of registration stages to perform.' + ' Options are:' + ' "rigid" (perform rigid registration only,' + ' which might be useful for intra-subject registration in longitudinal analysis);' + ' "affine" (perform affine registration);' + ' "nonlinear";' + f' as well as cominations of registration types: {registration_modes_string}.' + ' Default: rigid_affine_nonlinear', default='rigid_affine_nonlinear') options.add_argument('-voxel_size', type=app.Parser.SequenceFloat(), - help='Define the template voxel size in mm. ' - 'Use either a single value for isotropic voxels or 3 comma-separated values.') + help='Define the template voxel size in mm.' + ' Use either a single value for isotropic voxels or 3 comma-separated values.') options.add_argument('-initial_alignment', choices=INITIAL_ALIGNMENT, default='mass', - help='Method of alignment to form the initial template. ' - 'Options are: ' - '"mass" (default), ' - '"robust_mass" (requires masks), ' - '"geometric", ' - '"none".') + help='Method of alignment to form the initial template.' + ' Options are:' + ' "mass" (default);' + ' "robust_mass" (requires masks);' + ' "geometric";' + ' "none".') options.add_argument('-mask_dir', type=app.Parser.DirectoryIn(), - help='Optionally input a set of masks inside a single directory, ' - 'one per input image ' - '(with the same file name prefix). ' - 'Using masks will speed up registration significantly. ' - 'Note that masks are used for registration, ' - 'not for aggregation. ' - 'To exclude areas from aggregation, ' - 'NaN-mask your input images.') + help='Optionally input a set of masks inside a single directory,' + ' one per input image' + ' (with the same file name prefix).' + ' Using masks will speed up registration significantly.' + ' Note that masks are used for registration,' + ' not for aggregation.' + ' To exclude areas from aggregation,' + ' NaN-mask your input images.') options.add_argument('-warp_dir', type=app.Parser.DirectoryOut(), - help='Output a directory containing warps from each input to the template. ' - 'If the folder does not exist it will be created') + help='Output a directory containing warps from each input to the template.' + ' If the folder does not exist it will be created') # TODO Would prefer for this to be exclusively a directory; # but to do so will need to provide some form of disambiguation of multi-contrast files options.add_argument('-transformed_dir', type=app.Parser.DirectoryOut(), - help='Output a directory containing the input images transformed to the template. ' - 'If the folder does not exist it will be created. ' - 'For multi-contrast registration, ' - 'this path will contain a sub-directory for the images per contrast.') + help='Output a directory containing the input images transformed to the template.' + ' If the folder does not exist it will be created.' + ' For multi-contrast registration,' + ' this path will contain a sub-directory for the images per contrast.') options.add_argument('-linear_transformations_dir', type=app.Parser.DirectoryOut(), - help='Output a directory containing the linear transformations used to generate the template. ' - 'If the folder does not exist it will be created') + help='Output a directory containing the linear transformations' + ' used to generate the template.' + ' If the folder does not exist it will be created') options.add_argument('-template_mask', type=app.Parser.ImageOut(), - help='Output a template mask. ' - 'Only works if -mask_dir has been input. ' - 'The template mask is computed as the intersection of all subject masks in template space.') + help='Output a template mask.' + ' Only works if -mask_dir has been input.' + ' The template mask is computed as the intersection' + ' of all subject masks in template space.') options.add_argument('-noreorientation', action='store_true', - help='Turn off FOD reorientation in mrregister. ' - 'Reorientation is on by default if the number of volumes in the 4th dimension ' - 'corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series ' - '(i.e. 6, 15, 28, 45, 66 etc)') + help='Turn off FOD reorientation in mrregister.' + ' Reorientation is on by default if the number of volumes in the 4th dimension' + ' corresponds to the number of coefficients' + ' in an antipodally symmetric spherical harmonic series' + ' (i.e. 6, 15, 28, 45, 66 etc)') options.add_argument('-leave_one_out', choices=LEAVE_ONE_OUT, default='auto', - help='Register each input image to a template that does not contain that image. ' - f'Valid choices: {", ".join(LEAVE_ONE_OUT)}. ' - '(Default: auto (true if n_subjects larger than 2 and smaller than 15))') + help='Register each input image to a template that does not contain that image.' + f' Valid choices: {", ".join(LEAVE_ONE_OUT)}.' + ' (Default: auto (true if n_subjects larger than 2 and smaller than 15))') options.add_argument('-aggregate', choices=AGGREGATION_MODES, - help='Measure used to aggregate information from transformed images to the template image. ' - f'Valid choices: {", ".join(AGGREGATION_MODES)}. ' - 'Default: mean') + help='Measure used to aggregate information from transformed images to the template image.' + f' Valid choices: {", ".join(AGGREGATION_MODES)}.' + ' Default: mean') options.add_argument('-aggregation_weights', type=app.Parser.FileIn(), - help='Comma-separated file containing weights used for weighted image aggregation. ' - 'Each row must contain the identifiers of the input image and its weight. ' - 'Note that this weighs intensity values not transformations (shape).') + help='Comma-separated file containing weights used for weighted image aggregation.' + ' Each row must contain the identifiers of the input image and its weight.' + ' Note that this weighs intensity values not transformations (shape).') options.add_argument('-nanmask', action='store_true', - help='Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. ' - 'Only works if -mask_dir has been input.') + help='Optionally apply masks to (transformed) input images using NaN values' + ' to specify include areas for registration and aggregation.' + ' Only works if -mask_dir has been input.') options.add_argument('-copy_input', action='store_true', help='Copy input images and masks into local scratch directory.') diff --git a/python/bin/responsemean b/python/bin/responsemean index 627d47e766..d34eaff2d6 100755 --- a/python/bin/responsemean +++ b/python/bin/responsemean @@ -22,16 +22,16 @@ import math, sys def usage(cmdline): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' - 'and David Raffelt (david.raffelt@florey.edu.au)') + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)' + ' and David Raffelt (david.raffelt@florey.edu.au)') cmdline.set_synopsis('Calculate the mean response function from a set of text files') - cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines), ' - 'as well as the same number of coefficients per line.') - cmdline.add_description('As long as the number of unique b-values is identical across all input files, ' - 'the coefficients will be averaged. ' - 'This is performed on the assumption that the actual acquired b-values are identical. ' - 'This is however impossible for the responsemean command to determine based on the data provided; ' - 'it is therefore up to the user to ensure that this requirement is satisfied.') + cmdline.add_description('All response function files provided must contain the same number of unique b-values (lines),' + ' as well as the same number of coefficients per line.') + cmdline.add_description('As long as the number of unique b-values is identical across all input files,' + ' the response functions will be averaged.' + ' This is performed on the assumption that the actual acquired b-values are identical.' + ' This is however impossible for the responsemean command to determine based on the data provided;' + ' it is therefore up to the user to ensure that this requirement is satisfied.') cmdline.add_example_usage('Usage where all response functions are in the same directory:', 'responsemean input_response1.txt input_response2.txt input_response3.txt output_average_response.txt') cmdline.add_example_usage('Usage selecting response functions within a directory using a wildcard:', @@ -47,9 +47,9 @@ def usage(cmdline): #pylint: disable=unused-variable help='The output mean response function file') cmdline.add_argument('-legacy', action='store_true', - help='Use the legacy behaviour of former command "average_response": ' - 'average response function coefficients directly, ' - 'without compensating for global magnitude differences between input files') + help='Use the legacy behaviour of former command "average_response":' + ' average response function coefficients directly,' + ' without compensating for global magnitude differences between input files') diff --git a/python/lib/mrtrix3/dwi2mask/b02template.py b/python/lib/mrtrix3/dwi2mask/b02template.py index 019cea50ed..b086fe0e7b 100644 --- a/python/lib/mrtrix3/dwi2mask/b02template.py +++ b/python/lib/mrtrix3/dwi2mask/b02template.py @@ -54,16 +54,19 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('b02template', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') parser.set_synopsis('Register the mean b=0 image to a T2-weighted template to back-propagate a brain mask') - parser.add_description('This script currently assumes that the template image provided via the first input to the -template option is T2-weighted, ' + parser.add_description('This script currently assumes that the template image ' + 'provided via the first input to the -template option is T2-weighted, ' 'and can therefore be trivially registered to a mean b=0 image.') - parser.add_description('Command-line option -ants_options can be used with either the "antsquick" or "antsfull" software options. ' + parser.add_description('Command-line option -ants_options can be used ' + 'with either the "antsquick" or "antsfull" software options. ' 'In both cases, image dimensionality is assumed to be 3, ' 'and so this should be omitted from the user-specified options.' 'The input can be either a string ' '(encased in double-quotes if more than one option is specified), ' 'or a path to a text file containing the requested options. ' 'In the case of the "antsfull" software option, ' - 'one will require the names of the fixed and moving images that are provided to the antsRegistration command: ' + 'one will require the names of the fixed and moving images ' + 'that are provided to the antsRegistration command: ' 'these are "template_image.nii" and "bzero.nii" respectively.') parser.add_citation('M. Jenkinson, C.F. Beckmann, T.E. Behrens, M.W. Woolrich, S.M. Smith. ' 'FSL. ' diff --git a/python/lib/mrtrix3/dwi2mask/consensus.py b/python/lib/mrtrix3/dwi2mask/consensus.py index 10a4b0c7f5..db9995bcd0 100644 --- a/python/lib/mrtrix3/dwi2mask/consensus.py +++ b/python/lib/mrtrix3/dwi2mask/consensus.py @@ -33,7 +33,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options.add_argument('-algorithms', type=str, nargs='+', - help='Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised') + help='Provide a (space- or comma-separated) list ' + 'of dwi2mask algorithms that are to be utilised') options.add_argument('-masks', type=app.Parser.ImageOut(), metavar='image', @@ -46,7 +47,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), default=DEFAULT_THRESHOLD, - help='The fraction of algorithms that must include a voxel for that voxel to be present in the final mask ' + help='The fraction of algorithms that must include a voxel ' + 'for that voxel to be present in the final mask ' f'(default: {DEFAULT_THRESHOLD})') diff --git a/python/lib/mrtrix3/dwi2mask/mtnorm.py b/python/lib/mrtrix3/dwi2mask/mtnorm.py index 0503d881c1..f99a41dc81 100644 --- a/python/lib/mrtrix3/dwi2mask/mtnorm.py +++ b/python/lib/mrtrix3/dwi2mask/mtnorm.py @@ -38,9 +38,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'and subsequent mask image cleaning operations are performed.') parser.add_description('The operation of this script is a subset of that performed by the script "dwibiasnormmask". ' 'Many users may find that comprehensive solution preferable; ' - 'this dwi2mask algorithm is nevertheless provided to demonstrate specifically the mask estimation portion of that command.') - parser.add_description('The ODFs estimated within this optimisation procedure are by default of lower maximal spherical harmonic ' - 'degree than what would be advised for analysis. ' + 'this dwi2mask algorithm is nevertheless provided ' + 'to demonstrate specifically the mask estimation portion of that command.') + parser.add_description('The ODFs estimated within this optimisation procedure are by default ' + 'of lower maximal spherical harmonic degree than what would be advised for analysis. ' 'This is done for computational efficiency. ' 'This behaviour can be modified through the -lmax command-line option.') parser.add_citation('Jeurissen, B; Tournier, J-D; Dhollander, T; Connelly, A & Sijbers, J. ' diff --git a/python/lib/mrtrix3/dwi2mask/trace.py b/python/lib/mrtrix3/dwi2mask/trace.py index 923621baf7..aa760e9dde 100644 --- a/python/lib/mrtrix3/dwi2mask/trace.py +++ b/python/lib/mrtrix3/dwi2mask/trace.py @@ -46,7 +46,9 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable iter_options = parser.add_argument_group('Options for turning "dwi2mask trace" into an iterative algorithm') iter_options.add_argument('-iterative', action='store_true', - help='(EXPERIMENTAL) Iteratively refine the weights for combination of per-shell trace-weighted images prior to thresholding') + help='(EXPERIMENTAL) ' + 'Iteratively refine the weights for combination of per-shell trace-weighted images ' + 'prior to thresholding') iter_options.add_argument('-max_iters', type=app.Parser.Int(1), default=DEFAULT_MAX_ITERS, diff --git a/python/lib/mrtrix3/dwi2response/dhollander.py b/python/lib/mrtrix3/dwi2response/dhollander.py index 4dbb1ce73a..a0b2634d9d 100644 --- a/python/lib/mrtrix3/dwi2response/dhollander.py +++ b/python/lib/mrtrix3/dwi2response/dhollander.py @@ -31,10 +31,13 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.set_author('Thijs Dhollander (thijs.dhollander@gmail.com)') parser.set_synopsis('Unsupervised estimation of WM, GM and CSF response functions ' 'that does not require a T1 image (or segmentation thereof)') - parser.add_description('This is an improved version of the Dhollander et al. (2016) algorithm for unsupervised estimation of WM, GM and CSF response functions, ' - 'which includes the Dhollander et al. (2019) improvements for single-fibre WM response function estimation ' - '(prior to this update, ' - 'the "dwi2response tournier" algorithm had been utilised specifically for the single-fibre WM response function estimation step).') + parser.add_description('This is an improved version of the Dhollander et al. (2016) algorithm' + ' for unsupervised estimation of WM, GM and CSF response functions,' + ' which includes the Dhollander et al. (2019) improvements' + ' for single-fibre WM response function estimation' + ' (prior to this update,' + ' the "dwi2response tournier" algorithm had been utilised' + ' specifically for the single-fibre WM response function estimation step).') parser.add_citation('Dhollander, T.; Raffelt, D. & Connelly, A. ' 'Unsupervised 3-tissue response function estimation from single-shell or multi-shell diffusion MR data without a co-registered T1 image. ' 'ISMRM Workshop on Breaking the Barriers of Diffusion MRI, 2016, 5') diff --git a/python/lib/mrtrix3/dwi2response/tax.py b/python/lib/mrtrix3/dwi2response/tax.py index 7310f473a5..2fb4248517 100644 --- a/python/lib/mrtrix3/dwi2response/tax.py +++ b/python/lib/mrtrix3/dwi2response/tax.py @@ -24,7 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('tax', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - parser.set_synopsis('Use the Tax et al. (2014) recursive calibration algorithm for single-fibre voxel selection and response function estimation') + parser.set_synopsis('Use the Tax et al. (2014) recursive calibration algorithm' + ' for single-fibre voxel selection and response function estimation') parser.add_citation('Tax, C. M.; Jeurissen, B.; Vos, S. B.; Viergever, M. A. & Leemans, A. ' 'Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. ' 'NeuroImage, 2014, 86, 67-80') diff --git a/python/lib/mrtrix3/dwi2response/tournier.py b/python/lib/mrtrix3/dwi2response/tournier.py index 333c48a603..7ef04eb76c 100644 --- a/python/lib/mrtrix3/dwi2response/tournier.py +++ b/python/lib/mrtrix3/dwi2response/tournier.py @@ -24,7 +24,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('tournier', parents=[base_parser]) parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)') - parser.set_synopsis('Use the Tournier et al. (2013) iterative algorithm for single-fibre voxel selection and response function estimation') + parser.set_synopsis('Use the Tournier et al. (2013) iterative algorithm' + ' for single-fibre voxel selection and response function estimation') parser.add_citation('Tournier, J.-D.; Calamante, F. & Connelly, A. ' 'Determination of the appropriate b-value and number of gradient directions for high-angular-resolution diffusion-weighted imaging. ' 'NMR Biomedicine, 2013, 26, 1775-1786') diff --git a/python/lib/mrtrix3/dwibiascorrect/ants.py b/python/lib/mrtrix3/dwibiascorrect/ants.py index 784e890ee9..4fbf2c7233 100644 --- a/python/lib/mrtrix3/dwibiascorrect/ants.py +++ b/python/lib/mrtrix3/dwibiascorrect/ants.py @@ -21,7 +21,8 @@ OPT_N4_BIAS_FIELD_CORRECTION = { 's': ('4','shrink-factor applied to spatial dimensions'), - 'b': ('[100,3]','[initial mesh resolution in mm, spline order] This value is optimised for human adult data and needs to be adjusted for rodent data.'), + 'b': ('[100,3]','[initial mesh resolution in mm, spline order]' + ' This value is optimised for human adult data and needs to be adjusted for rodent data.'), 'c': ('[1000,0.0]', '[numberOfIterations,convergenceThreshold]')} diff --git a/python/lib/mrtrix3/dwibiascorrect/mtnorm.py b/python/lib/mrtrix3/dwibiascorrect/mtnorm.py index 59bd443628..d86125f9bf 100644 --- a/python/lib/mrtrix3/dwibiascorrect/mtnorm.py +++ b/python/lib/mrtrix3/dwibiascorrect/mtnorm.py @@ -22,8 +22,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mtnorm', parents=[base_parser]) - parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' - 'and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') + parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)' + ' and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') parser.set_synopsis('Perform DWI bias field correction using the "mtnormalise" command') parser.add_description('This algorithm bases its operation almost entirely on the utilisation of multi-tissue ' 'decomposition information to estimate an underlying B1 receive field, ' diff --git a/python/lib/mrtrix3/dwinormalise/mtnorm.py b/python/lib/mrtrix3/dwinormalise/mtnorm.py index 49f31a085d..e8e01647b3 100644 --- a/python/lib/mrtrix3/dwinormalise/mtnorm.py +++ b/python/lib/mrtrix3/dwinormalise/mtnorm.py @@ -26,8 +26,8 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser = subparsers.add_parser('mtnorm', parents=[base_parser]) - parser.set_author('Robert E. Smith (robert.smith@florey.edu.au) ' - 'and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') + parser.set_author('Robert E. Smith (robert.smith@florey.edu.au)' + ' and Arshiya Sangchooli (asangchooli@student.unimelb.edu.au)') parser.set_synopsis('Normalise a DWI series to the estimated b=0 CSF intensity') parser.add_description('This algorithm determines an appropriate global scaling factor to apply to a DWI series ' 'such that after the scaling is applied, ' From 9822d58ab1061acdd044111bafd6466210196f74 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 28 Feb 2024 17:55:28 +1100 Subject: [PATCH 39/75] Python CLI: Better metavar handling These changes should provide superior guarantees around the presentation of command-line options, both in help pages and in exported interface documentation. --- docs/reference/commands/5ttgen.rst | 4 +- docs/reference/commands/dwi2mask.rst | 34 ++--- docs/reference/commands/dwi2response.rst | 6 +- docs/reference/commands/dwibiasnormmask.rst | 10 +- docs/reference/commands/dwifslpreproc.rst | 2 +- docs/reference/commands/dwigradcheck.rst | 2 +- docs/reference/commands/dwishellmath.rst | 2 +- docs/reference/commands/mask2glass.rst | 6 +- .../commands/population_template.rst | 56 +++---- python/bin/dwi2response | 3 - python/bin/dwibiascorrect | 2 - python/bin/dwibiasnormmask | 7 - python/bin/dwicat | 1 - python/bin/dwifslpreproc | 6 - python/bin/dwigradcheck | 2 +- python/bin/mask2glass | 3 +- python/bin/mrtrix_cleanup | 1 - python/bin/population_template | 4 - python/lib/mrtrix3/_5ttgen/freesurfer.py | 1 - python/lib/mrtrix3/_5ttgen/fsl.py | 2 - python/lib/mrtrix3/_5ttgen/hsvs.py | 1 - python/lib/mrtrix3/app.py | 143 +++++++++++------- python/lib/mrtrix3/dwi2mask/3dautomask.py | 4 + python/lib/mrtrix3/dwi2mask/consensus.py | 1 - python/lib/mrtrix3/dwi2mask/mtnorm.py | 4 - python/lib/mrtrix3/dwi2mask/synthstrip.py | 1 - python/lib/mrtrix3/dwi2mask/trace.py | 1 + python/lib/mrtrix3/dwi2response/dhollander.py | 2 +- python/lib/mrtrix3/dwi2response/fa.py | 3 +- python/lib/mrtrix3/dwi2response/manual.py | 1 - python/lib/mrtrix3/dwi2response/msmt_5tt.py | 3 - python/lib/mrtrix3/dwi2response/tax.py | 1 - python/lib/mrtrix3/dwi2response/tournier.py | 2 +- python/lib/mrtrix3/dwibiascorrect/mtnorm.py | 1 - python/lib/mrtrix3/dwinormalise/group.py | 1 - python/lib/mrtrix3/dwinormalise/manual.py | 2 - python/lib/mrtrix3/dwinormalise/mtnorm.py | 4 - 37 files changed, 161 insertions(+), 168 deletions(-) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index 83bbf1829a..435669fd4a 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -395,9 +395,9 @@ Options - **-template image** Provide an image that will form the template for the generated 5TT image -- **-hippocampi** Select method to be used for hippocampi (& amygdalae) segmentation; options are: subfields,first,aseg +- **-hippocampi choice** Select method to be used for hippocampi (& amygdalae) segmentation; options are: subfields,first,aseg -- **-thalami** Select method to be used for thalamic segmentation; options are: nuclei,first,aseg +- **-thalami choice** Select method to be used for thalamic segmentation; options are: nuclei,first,aseg - **-white_stem** Classify the brainstem as white matter diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index 6325cd6270..4e73a7fc73 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -115,21 +115,21 @@ Options Options specific to the "3dautomask" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-clfrac** Set the "clip level fraction"; must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger. +- **-clfrac value** Set the "clip level fraction"; must be a number between 0.1 and 0.9. A small value means to make the initial threshold for clipping smaller, which will tend to make the mask larger. - **-nograd** The program uses a "gradual" clip level by default. Add this option to use a fixed clip level. -- **-peels** Peel (erode) the mask n times, then unpeel (dilate). +- **-peels iterations** Peel (erode) the mask n times, then unpeel (dilate). -- **-nbhrs** Define the number of neighbors needed for a voxel NOT to be eroded. It should be between 6 and 26. +- **-nbhrs count** Define the number of neighbors needed for a voxel NOT to be eroded. It should be between 6 and 26. - **-eclip** After creating the mask, remove exterior voxels below the clip threshold. -- **-SI** After creating the mask, find the most superior voxel, then zero out everything more than SI millimeters inferior to that. 130 seems to be decent (i.e., for Homo Sapiens brains). +- **-SI value** After creating the mask, find the most superior voxel, then zero out everything more than SI millimeters inferior to that. 130 seems to be decent (i.e., for Homo Sapiens brains). -- **-dilate** Dilate the mask outwards n times +- **-dilate iterations** Dilate the mask outwards n times -- **-erode** Erode the mask outwards n times +- **-erode iterations** Erode the mask outwards n times - **-NN1** Erode and dilate based on mask faces @@ -336,7 +336,7 @@ Options applicable when using the ANTs software for registration Options specific to the "template" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-software** The software to use for template registration; options are: antsfull,antsquick,fsl; default is antsquick +- **-software choice** The software to use for template registration; options are: antsfull,antsquick,fsl; default is antsquick - **-template TemplateImage MaskImage** Provide the template image to which the input data will be registered, and the mask to be projected to the input image. The template image should be T2-weighted. @@ -437,7 +437,7 @@ Options specific to the "consensus" algorithm - **-template TemplateImage MaskImage** Provide a template image and corresponding mask for those algorithms requiring such -- **-threshold** The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: 0.501) +- **-threshold value** The fraction of algorithms that must include a voxel for that voxel to be present in the final mask (default: 0.501) Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -526,13 +526,13 @@ Options Options specific to the "fslbet" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-bet_f** Fractional intensity threshold (0->1); smaller values give larger brain outline estimates +- **-bet_f value** Fractional intensity threshold (0->1); smaller values give larger brain outline estimates -- **-bet_g** Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top +- **-bet_g value** Vertical gradient in fractional intensity threshold (-1->1); positive values give larger brain outline at bottom, smaller at top - **-bet_c i,j,k** Centre-of-gravity (voxels not mm) of initial mesh surface -- **-bet_r** Head radius (mm not voxels); initial surface sphere is set to half of this +- **-bet_r value** Head radius (mm not voxels); initial surface sphere is set to half of this - **-rescale** Rescale voxel size provided to BET to 1mm isotropic; can improve results for rodent data @@ -713,7 +713,7 @@ Usage Options ------- -- **-clean_scale** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) +- **-clean_scale value** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -804,7 +804,7 @@ Options specific to the "mean" algorithm - **-shells bvalues** Comma separated list of shells to be included in the volume averaging -- **-clean_scale** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) +- **-clean_scale value** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1006,7 +1006,7 @@ Options Options specific to the 'Synthstrip' algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-stripped** The output stripped image +- **-stripped image** The output stripped image - **-gpu** Use the GPU @@ -1014,7 +1014,7 @@ Options specific to the 'Synthstrip' algorithm - **-nocsf** Compute the immediate boundary of brain matter excluding surrounding CSF -- **-border** Control the boundary distance from the brain +- **-border value** Control the boundary distance from the brain Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1107,14 +1107,14 @@ Options for turning "dwi2mask trace" into an iterative algorithm - **-iterative** (EXPERIMENTAL) Iteratively refine the weights for combination of per-shell trace-weighted images prior to thresholding -- **-max_iters** Set the maximum number of iterations for the algorithm (default: 10) +- **-max_iters iterations** Set the maximum number of iterations for the algorithm (default: 10) Options specific to the "trace" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - **-shells bvalues** Comma-separated list of shells used to generate trace-weighted images for masking -- **-clean_scale** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) +- **-clean_scale value** the maximum scale used to cut bridges. A certain maximum scale cuts bridges up to a width (in voxels) of 2x the provided scale. Setting this to 0 disables the mask cleaning step. (Default: 2) Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index ce15f18d44..472cf7352e 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -138,7 +138,7 @@ Options Options for the "dhollander" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-erode passes** Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3) +- **-erode iterations** Number of erosion passes to apply to initial (whole brain) mask. Set to 0 to not erode the brain mask. (default: 3) - **-fa threshold** FA threshold for crude WM versus GM-CSF separation. (default: 0.2) @@ -252,7 +252,7 @@ Options Options specific to the "fa" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-erode passes** Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually) +- **-erode iterations** Number of brain mask erosion steps to apply prior to threshold (not used if mask is provided manually) - **-number voxels** The number of highest-FA voxels to use @@ -682,7 +682,7 @@ Options specific to the "tournier" algorithm - **-iter_voxels voxels** Number of single-fibre voxels to select when preparing for the next iteration (default = 10 x value given in -number) -- **-dilate passes** Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration +- **-dilate iterations** Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration - **-max_iters iterations** Maximum number of iterations (set to 0 to force convergence) diff --git a/docs/reference/commands/dwibiasnormmask.rst b/docs/reference/commands/dwibiasnormmask.rst index 7016a43c59..0675473834 100644 --- a/docs/reference/commands/dwibiasnormmask.rst +++ b/docs/reference/commands/dwibiasnormmask.rst @@ -47,9 +47,9 @@ Options for importing the diffusion gradient table Options relevant to the internal optimisation procedure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-dice value** Set the Dice coefficient threshold for similarity of masks between sequential iterations that will result in termination due to convergence; default = 0.999 +- **-dice value** Set the Dice coefficient threshold for similarity of masks between sequential iterations that will result in termination due to convergence; default = 0.999 -- **-init_mask** Provide an initial mask for the first iteration of the algorithm (if not provided, the default dwi2mask algorithm will be used) +- **-init_mask image** Provide an initial mask for the first iteration of the algorithm (if not provided, the default dwi2mask algorithm will be used) - **-max_iters count** The maximum number of iterations (see Description); default is 2; set to 0 to proceed until convergence @@ -60,11 +60,11 @@ Options relevant to the internal optimisation procedure Options that modulate the outputs of the script ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-output_bias** Export the final estimated bias field to an image +- **-output_bias image** Export the final estimated bias field to an image -- **-output_scale** Write the scaling factor applied to the DWI series to a text file +- **-output_scale file** Write the scaling factor applied to the DWI series to a text file -- **-output_tissuesum** Export the tissue sum image that was used to generate the final mask +- **-output_tissuesum image** Export the tissue sum image that was used to generate the final mask - **-reference value** Set the target CSF b=0 intensity in the output DWI series (default: 1000.0) diff --git a/docs/reference/commands/dwifslpreproc.rst b/docs/reference/commands/dwifslpreproc.rst index 81927c4f0a..ac43dd280e 100644 --- a/docs/reference/commands/dwifslpreproc.rst +++ b/docs/reference/commands/dwifslpreproc.rst @@ -35,7 +35,7 @@ The "-topup_options" and "-eddy_options" command-line options allow the user to The script will attempt to run the CUDA version of eddy; if this does not succeed for any reason, or is not present on the system, the CPU version will be attempted instead. By default, the CUDA eddy binary found that indicates compilation against the most recent version of CUDA will be attempted; this can be over-ridden by providing a soft-link "eddy_cuda" within your path that links to the binary you wish to be executed. -Note that this script does not perform any explicit registration between images provided to topup via the -se_epi option, and the DWI volumes provided to eddy. In some instances (motion between acquisitions) this can result in erroneous application of the inhomogeneity field during distortion correction. Use of the -align_seepi option is advocated in this scenario, which ensures that the first volume in the series provided to topup is also the first volume in the series provided to eddy, guaranteeing alignment. But a prerequisite for this approach is that the image contrast within the images provided to the -se_epi option must match the b=0 volumes present within the input DWI series: this means equivalent TE, TR and flip angle (note that differences in multi-band factors between two acquisitions may lead to differences in TR). +Note that this script does not perform any explicit registration between images provided to topup via the -se_epi option, and the DWI volumes provided to eddy. In some instances (motion between acquisitions) this can result in erroneous application of the inhomogeneity field during distortion correction. Use of the -align_seepi option is advocated in this scenario, which ensures that the first volume in the series provided to topup is also the first volume in the series provided to eddy, guaranteeing alignment. But a prerequisite for this approach is that the image contrast within the images provided to the -se_epi option must match the b=0 volumes present within the input DWI series: this means equivalent TE, TR and flip angle (note that differences in multi-band factors between two acquisitions may lead to differences in TR). Example usages -------------- diff --git a/docs/reference/commands/dwigradcheck.rst b/docs/reference/commands/dwigradcheck.rst index cdf020fcf4..1317b9e73c 100644 --- a/docs/reference/commands/dwigradcheck.rst +++ b/docs/reference/commands/dwigradcheck.rst @@ -30,7 +30,7 @@ Options - **-mask image** Provide a mask image within which to seed & constrain tracking -- **-number** Set the number of tracks to generate for each test +- **-number count** Set the number of tracks to generate for each test Options for importing the diffusion gradient table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/dwishellmath.rst b/docs/reference/commands/dwishellmath.rst index 005820818b..4c6611b63f 100644 --- a/docs/reference/commands/dwishellmath.rst +++ b/docs/reference/commands/dwishellmath.rst @@ -22,7 +22,7 @@ Usage Description ----------- -The output of this command is a 4D image, where each volume corresponds to a b-value shell (in order of increasing b-value), an the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell. +The output of this command is a 4D image, where each volume corresponds to a b-value shell (in order of increasing b-value), and the intensities within each volume correspond to the chosen statistic having been computed from across the DWI volumes belonging to that b-value shell. Example usages -------------- diff --git a/docs/reference/commands/mask2glass.rst b/docs/reference/commands/mask2glass.rst index e774d3e349..8257bf5f85 100644 --- a/docs/reference/commands/mask2glass.rst +++ b/docs/reference/commands/mask2glass.rst @@ -28,11 +28,11 @@ While the name of this script indicates that a binary mask image is required as Options ------- -- **-dilate** Provide number of passes for dilation step; default = 2 +- **-dilate iterations** Provide number of iterations for dilation step; default = 2 -- **-scale** Provide resolution upscaling value; default = 2.0 +- **-scale value** Provide resolution upscaling value; default = 2.0 -- **-smooth** Provide standard deviation of smoothing (in mm); default = 1.0 +- **-smooth value** Provide standard deviation of smoothing (in mm); default = 1.0 Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/reference/commands/population_template.rst b/docs/reference/commands/population_template.rst index 9e38019957..80bf22150a 100644 --- a/docs/reference/commands/population_template.rst +++ b/docs/reference/commands/population_template.rst @@ -38,29 +38,29 @@ Options Input, output and general options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-type** Specify the types of registration stages to perform. Options are: "rigid" (perform rigid registration only, which might be useful for intra-subject registration in longitudinal analysis); "affine" (perform affine registration); "nonlinear"; as well as cominations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear +- **-type choice** Specify the types of registration stages to perform. Options are: "rigid" (perform rigid registration only, which might be useful for intra-subject registration in longitudinal analysis); "affine" (perform affine registration); "nonlinear"; as well as cominations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear -- **-voxel_size** Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values. +- **-voxel_size values** Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values. -- **-initial_alignment** Method of alignment to form the initial template.Options are: "mass" (default); "robust_mass" (requires masks); "geometric"; "none". +- **-initial_alignment choice** Method of alignment to form the initial template. Options are: "mass" (default); "robust_mass" (requires masks); "geometric"; "none". -- **-mask_dir** Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images. +- **-mask_dir directory** Optionally input a set of masks inside a single directory, one per input image (with the same file name prefix). Using masks will speed up registration significantly. Note that masks are used for registration, not for aggregation. To exclude areas from aggregation, NaN-mask your input images. -- **-warp_dir** Output a directory containing warps from each input to the template. If the folder does not exist it will be created +- **-warp_dir directory** Output a directory containing warps from each input to the template. If the folder does not exist it will be created -- **-transformed_dir** Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, this path will contain a sub-directory for the images per contrast. +- **-transformed_dir directory** Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, this path will contain a sub-directory for the images per contrast. -- **-linear_transformations_dir** Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created +- **-linear_transformations_dir directory** Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created -- **-template_mask** Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space. +- **-template_mask image** Output a template mask. Only works if -mask_dir has been input. The template mask is computed as the intersection of all subject masks in template space. - **-noreorientation** Turn off FOD reorientation in mrregister. Reorientation is on by default if the number of volumes in the 4th dimension corresponds to the number of coefficients in an antipodally symmetric spherical harmonic series (i.e. 6, 15, 28, 45, 66 etc) -- **-leave_one_out** Register each input image to a template that does not contain that image. Valid choices: 0, 1, auto. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) +- **-leave_one_out choice** Register each input image to a template that does not contain that image. Valid choices: 0, 1, auto. (Default: auto (true if n_subjects larger than 2 and smaller than 15)) -- **-aggregate** Measure used to aggregate information from transformed images to the template image. Valid choices: mean, median. Default: mean +- **-aggregate choice** Measure used to aggregate information from transformed images to the template image. Valid choices: mean, median. Default: mean -- **-aggregation_weights** Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape). +- **-aggregation_weights file** Comma-separated file containing weights used for weighted image aggregation. Each row must contain the identifiers of the input image and its weight. Note that this weighs intensity values not transformations (shape). - **-nanmask** Optionally apply masks to (transformed) input images using NaN values to specify include areas for registration and aggregation. Only works if -mask_dir has been input. @@ -71,17 +71,17 @@ Input, output and general options Options for the non-linear registration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-nl_scale** Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0). This implicitly defines the number of template levels +- **-nl_scale values** Specify the multi-resolution pyramid used to build the non-linear template, in the form of a list of scale factors (default: 0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0). This implicitly defines the number of template levels -- **-nl_lmax** Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: 2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4). The list must be the same length as the nl_scale factor list +- **-nl_lmax values** Specify the lmax used for non-linear registration for each scale factor, in the form of a list of integers (default: 2,2,2,2,2,2,2,2,4,4,4,4,4,4,4,4). The list must be the same length as the nl_scale factor list -- **-nl_niter** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5). The list must be the same length as the nl_scale factor list +- **-nl_niter values** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5). The list must be the same length as the nl_scale factor list -- **-nl_update_smooth** Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default 2.0 x voxel_size) +- **-nl_update_smooth value** Regularise the gradient update field with Gaussian smoothing (standard deviation in voxel units, Default 2.0 x voxel_size) -- **-nl_disp_smooth** Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default 1.0 x voxel_size) +- **-nl_disp_smooth value** Regularise the displacement field with Gaussian smoothing (standard deviation in voxel units, Default 1.0 x voxel_size) -- **-nl_grad_step** The gradient step size for non-linear registration (Default: 0.5) +- **-nl_grad_step value** The gradient step size for non-linear registration (Default: 0.5) Options for the linear registration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -90,30 +90,30 @@ Options for the linear registration - **-linear_no_drift_correction** Deactivate correction of template appearance (scale and shear) over iterations -- **-linear_estimator** Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: \|x\|), l2 (ordinary least squares), lp (least powers: \|x\|^1.2), none (no robust estimator). Default: none. +- **-linear_estimator choice** Specify estimator for intensity difference metric. Valid choices are: l1 (least absolute: \|x\|), l2 (ordinary least squares), lp (least powers: \|x\|^1.2), none (no robust estimator). Default: none. -- **-rigid_scale** Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: 0.3,0.4,0.6,0.8,1.0,1.0). This and affine_scale implicitly define the number of template levels +- **-rigid_scale values** Specify the multi-resolution pyramid used to build the rigid template, in the form of a list of scale factors (default: 0.3,0.4,0.6,0.8,1.0,1.0). This and affine_scale implicitly define the number of template levels -- **-rigid_lmax** Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: 2,2,2,4,4,4). The list must be the same length as the linear_scale factor list +- **-rigid_lmax values** Specify the lmax used for rigid registration for each scale factor, in the form of a list of integers (default: 2,2,2,4,4,4). The list must be the same length as the linear_scale factor list -- **-rigid_niter** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 50 for each scale). This must be a single number or a list of same length as the linear_scale factor list +- **-rigid_niter values** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 50 for each scale). This must be a single number or a list of same length as the linear_scale factor list -- **-affine_scale** Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: 0.3,0.4,0.6,0.8,1.0,1.0). This and rigid_scale implicitly define the number of template levels +- **-affine_scale values** Specify the multi-resolution pyramid used to build the affine template, in the form of a list of scale factors (default: 0.3,0.4,0.6,0.8,1.0,1.0). This and rigid_scale implicitly define the number of template levels -- **-affine_lmax** Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: 2,2,2,4,4,4). The list must be the same length as the linear_scale factor list +- **-affine_lmax values** Specify the lmax used for affine registration for each scale factor, in the form of a list of integers (default: 2,2,2,4,4,4). The list must be the same length as the linear_scale factor list -- **-affine_niter** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 500 for each scale). This must be a single number or a list of same length as the linear_scale factor list +- **-affine_niter values** Specify the number of registration iterations used within each level before updating the template, in the form of a list of integers (default: 500 for each scale). This must be a single number or a list of same length as the linear_scale factor list Multi-contrast options ^^^^^^^^^^^^^^^^^^^^^^ -- **-mc_weight_initial_alignment** Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0 for each contrast (ie. equal weighting). +- **-mc_weight_initial_alignment values** Weight contribution of each contrast to the initial alignment. Comma separated, default: 1.0 for each contrast (ie. equal weighting). -- **-mc_weight_rigid** Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) +- **-mc_weight_rigid values** Weight contribution of each contrast to the objective of rigid registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) -- **-mc_weight_affine** Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) +- **-mc_weight_affine values** Weight contribution of each contrast to the objective of affine registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) -- **-mc_weight_nl** Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) +- **-mc_weight_nl values** Weight contribution of each contrast to the objective of nonlinear registration. Comma separated, default: 1.0 for each contrast (ie. equal weighting) Additional standard options for Python scripts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/python/bin/dwi2response b/python/bin/dwi2response index c1e1a5f53e..443c0570d0 100755 --- a/python/bin/dwi2response +++ b/python/bin/dwi2response @@ -43,11 +43,9 @@ def usage(cmdline): #pylint: disable=unused-variable common_options = cmdline.add_argument_group('General dwi2response options') common_options.add_argument('-mask', type=app.Parser.ImageIn(), - metavar='image', help='Provide an initial mask for response voxel selection') common_options.add_argument('-voxels', type=app.Parser.ImageOut(), - metavar='image', help='Output an image showing the final voxel selection(s)') common_options.add_argument('-shells', type=app.Parser.SequenceFloat(), @@ -57,7 +55,6 @@ def usage(cmdline): #pylint: disable=unused-variable 'b=0 must be included explicitly if desired)') common_options.add_argument('-lmax', type=app.Parser.SequenceInt(), - metavar='values', help='The maximum harmonic degree(s) for response function estimation ' '(comma-separated list in case of multiple b-values)') app.add_dwgrad_import_options(cmdline) diff --git a/python/bin/dwibiascorrect b/python/bin/dwibiascorrect index ffc6015790..c8e1313684 100755 --- a/python/bin/dwibiascorrect +++ b/python/bin/dwibiascorrect @@ -29,11 +29,9 @@ def usage(cmdline): #pylint: disable=unused-variable common_options = cmdline.add_argument_group('Options common to all dwibiascorrect algorithms') common_options.add_argument('-mask', type=app.Parser.ImageIn(), - metavar='image', help='Manually provide an input mask image for bias field estimation') common_options.add_argument('-bias', type=app.Parser.ImageOut(), - metavar='image', help='Output an image containing the estimated bias field') app.add_dwgrad_import_options(cmdline) diff --git a/python/bin/dwibiasnormmask b/python/bin/dwibiasnormmask index e5c4e91dc6..d4b9585a24 100755 --- a/python/bin/dwibiasnormmask +++ b/python/bin/dwibiasnormmask @@ -116,19 +116,15 @@ def usage(cmdline): #pylint: disable=unused-variable output_options = cmdline.add_argument_group('Options that modulate the outputs of the script') output_options.add_argument('-output_bias', type=app.Parser.ImageOut(), - metavar='image', help='Export the final estimated bias field to an image') output_options.add_argument('-output_scale', type=app.Parser.FileOut(), - metavar='file', help='Write the scaling factor applied to the DWI series to a text file') output_options.add_argument('-output_tissuesum', type=app.Parser.ImageOut(), - metavar='image', help='Export the tissue sum image that was used to generate the final mask') output_options.add_argument('-reference', type=app.Parser.Float(0.0), - metavar='value', default=REFERENCE_INTENSITY, help='Set the target CSF b=0 intensity in the output DWI series' f' (default: {REFERENCE_INTENSITY})') @@ -136,13 +132,11 @@ def usage(cmdline): #pylint: disable=unused-variable internal_options.add_argument('-dice', type=app.Parser.Float(0.0, 1.0), default=DICE_COEFF_DEFAULT, - metavar='value', help='Set the Dice coefficient threshold for similarity of masks between sequential iterations' ' that will result in termination due to convergence;' f' default = {DICE_COEFF_DEFAULT}') internal_options.add_argument('-init_mask', type=app.Parser.ImageIn(), - metavar='image', help='Provide an initial mask for the first iteration of the algorithm' ' (if not provided, the default dwi2mask algorithm will be used)') internal_options.add_argument('-max_iters', @@ -159,7 +153,6 @@ def usage(cmdline): #pylint: disable=unused-variable ' potentially based on the ODF sum image (see Description);' f' default: {MASK_ALGO_DEFAULT}') internal_options.add_argument('-lmax', - metavar='values', type=app.Parser.SequenceInt(), help='The maximum spherical harmonic degree for the estimated FODs (see Description);' f' defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' diff --git a/python/bin/dwicat b/python/bin/dwicat index 810fe14ead..544534e2e3 100755 --- a/python/bin/dwicat +++ b/python/bin/dwicat @@ -42,7 +42,6 @@ def usage(cmdline): #pylint: disable=unused-variable type=app.Parser.ImageOut(), help='The output image series (all DWIs concatenated)') cmdline.add_argument('-mask', - metavar='image', type=app.Parser.ImageIn(), help='Provide a binary mask within which image intensities will be matched') diff --git a/python/bin/dwifslpreproc b/python/bin/dwifslpreproc index fd90aa5ae3..7cc770a8b7 100755 --- a/python/bin/dwifslpreproc +++ b/python/bin/dwifslpreproc @@ -180,7 +180,6 @@ def usage(cmdline): #pylint: disable=unused-variable help='The output corrected image series') cmdline.add_argument('-json_import', type=app.Parser.FileIn(), - metavar='file', help='Import image header information from an associated JSON file' ' (may be necessary to determine phase encoding information)') pe_options = cmdline.add_argument_group('Options for manually specifying the phase encoding of the input DWIs') @@ -197,7 +196,6 @@ def usage(cmdline): #pylint: disable=unused-variable distcorr_options = cmdline.add_argument_group('Options for achieving correction of susceptibility distortions') distcorr_options.add_argument('-se_epi', type=app.Parser.ImageIn(), - metavar='image', help='Provide an additional image series consisting of spin-echo EPI images,' ' which is to be used exclusively by topup for estimating the inhomogeneity field' ' (i.e. it will not form part of the output image series)') @@ -219,12 +217,10 @@ def usage(cmdline): #pylint: disable=unused-variable eddy_options = cmdline.add_argument_group('Options for affecting the operation of the FSL "eddy" command') eddy_options.add_argument('-eddy_mask', type=app.Parser.ImageIn(), - metavar='image', help='Provide a processing mask to use for eddy,' ' instead of having dwifslpreproc generate one internally using dwi2mask') eddy_options.add_argument('-eddy_slspec', type=app.Parser.FileIn(), - metavar='file', help='Provide a file containing slice groupings for eddy\'s slice-to-volume registration') eddy_options.add_argument('-eddy_options', metavar='" EddyOptions"', @@ -234,13 +230,11 @@ def usage(cmdline): #pylint: disable=unused-variable eddyqc_options = cmdline.add_argument_group('Options for utilising EddyQC') eddyqc_options.add_argument('-eddyqc_text', type=app.Parser.DirectoryOut(), - metavar='directory', help='Copy the various text-based statistical outputs generated by eddy,' ' and the output of eddy_qc (if installed),' ' into an output directory') eddyqc_options.add_argument('-eddyqc_all', type=app.Parser.DirectoryOut(), - metavar='directory', help='Copy ALL outputs generated by eddy (including images),' ' and the output of eddy_qc (if installed),' ' into an output directory') diff --git a/python/bin/dwigradcheck b/python/bin/dwigradcheck index 611530c62a..a6023e6f01 100755 --- a/python/bin/dwigradcheck +++ b/python/bin/dwigradcheck @@ -38,10 +38,10 @@ def usage(cmdline): #pylint: disable=unused-variable help='The input DWI series to be checked') cmdline.add_argument('-mask', type=app.Parser.ImageIn(), - metavar='image', help='Provide a mask image within which to seed & constrain tracking') cmdline.add_argument('-number', type=app.Parser.Int(1), + metavar='count', default=10000, help='Set the number of tracks to generate for each test') diff --git a/python/bin/mask2glass b/python/bin/mask2glass index d2c8fab50b..bff040d35a 100755 --- a/python/bin/mask2glass +++ b/python/bin/mask2glass @@ -37,8 +37,9 @@ def usage(cmdline): #pylint: disable=unused-variable help='The output glass brain image') cmdline.add_argument('-dilate', type=app.Parser.Int(0), + metavar='iterations', default=2, - help='Provide number of passes for dilation step;' + help='Provide number of iterations for dilation step;' ' default = 2') cmdline.add_argument('-scale', type=app.Parser.Float(0.0), diff --git a/python/bin/mrtrix_cleanup b/python/bin/mrtrix_cleanup index f1275414c1..a5d210fc04 100755 --- a/python/bin/mrtrix_cleanup +++ b/python/bin/mrtrix_cleanup @@ -51,7 +51,6 @@ def usage(cmdline): #pylint: disable=unused-variable ' but not attempt to delete them') cmdline.add_argument('-failed', type=app.Parser.FileOut(), - metavar='file', help='Write list of items that the script failed to delete to a text file') cmdline.flag_mutually_exclusive_options([ 'test', 'failed' ]) diff --git a/python/bin/population_template b/python/bin/population_template index d6219ea94a..afd46eb330 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -79,25 +79,21 @@ def usage(cmdline): #pylint: disable=unused-variable options = cmdline.add_argument_group('Multi-contrast options') options.add_argument('-mc_weight_initial_alignment', type=app.Parser.SequenceFloat(), - metavar='values', help='Weight contribution of each contrast to the initial alignment.' ' Comma separated,' ' default: 1.0 for each contrast (ie. equal weighting).') options.add_argument('-mc_weight_rigid', type=app.Parser.SequenceFloat(), - metavar='values', help='Weight contribution of each contrast to the objective of rigid registration.' ' Comma separated,' ' default: 1.0 for each contrast (ie. equal weighting)') options.add_argument('-mc_weight_affine', type=app.Parser.SequenceFloat(), - metavar='values', help='Weight contribution of each contrast to the objective of affine registration.' ' Comma separated,' ' default: 1.0 for each contrast (ie. equal weighting)') options.add_argument('-mc_weight_nl', type=app.Parser.SequenceFloat(), - metavar='values', help='Weight contribution of each contrast to the objective of nonlinear registration.' ' Comma separated,' ' default: 1.0 for each contrast (ie. equal weighting)') diff --git a/python/lib/mrtrix3/_5ttgen/freesurfer.py b/python/lib/mrtrix3/_5ttgen/freesurfer.py index ad343b4afa..7b0b670ec6 100644 --- a/python/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/python/lib/mrtrix3/_5ttgen/freesurfer.py @@ -33,7 +33,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "freesurfer" algorithm') options.add_argument('-lut', type=app.Parser.FileIn(), - metavar='file', help='Manually provide path to the lookup table on which the input parcellation image is based ' '(e.g. FreeSurferColorLUT.txt)') diff --git a/python/lib/mrtrix3/_5ttgen/fsl.py b/python/lib/mrtrix3/_5ttgen/fsl.py index 2b2341132a..3786e6249b 100644 --- a/python/lib/mrtrix3/_5ttgen/fsl.py +++ b/python/lib/mrtrix3/_5ttgen/fsl.py @@ -48,12 +48,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "fsl" algorithm') options.add_argument('-t2', type=app.Parser.ImageIn(), - metavar='image', help='Provide a T2-weighted image in addition to the default T1-weighted image; ' 'this will be used as a second input to FSL FAST') options.add_argument('-mask', type=app.Parser.ImageIn(), - metavar='image', help='Manually provide a brain mask, ' 'rather than deriving one in the script') options.add_argument('-premasked', diff --git a/python/lib/mrtrix3/_5ttgen/hsvs.py b/python/lib/mrtrix3/_5ttgen/hsvs.py index 62e51518b2..c258c133f2 100644 --- a/python/lib/mrtrix3/_5ttgen/hsvs.py +++ b/python/lib/mrtrix3/_5ttgen/hsvs.py @@ -43,7 +43,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable help='The output 5TT image') parser.add_argument('-template', type=app.Parser.ImageIn(), - metavar='image', help='Provide an image that will form the template for the generated 5TT image') parser.add_argument('-hippocampi', choices=HIPPOCAMPI_CHOICES, diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 28eff1b4ba..6d34066c98 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -648,7 +648,10 @@ class Parser(argparse.ArgumentParser): # Various callable types for use as argparse argument types class CustomTypeBase: @staticmethod - def _typestring(): + def _legacytypestring(): + assert False + @staticmethod + def _metavar(): assert False class Bool(CustomTypeBase): @@ -664,8 +667,11 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as boolean value"') from exc return bool(processed_value) @staticmethod - def _typestring(): + def _legacytypestring(): return 'BOOL' + @staticmethod + def _metavar(): + return 'value' def Int(min_value=None, max_value=None): # pylint: disable=invalid-name,no-self-argument assert min_value is None or isinstance(min_value, int) @@ -683,8 +689,11 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError(f'Input value "{input_value}" greater than maximum permissible value {max_value}') return value @staticmethod - def _typestring(): + def _legacytypestring(): return f'INT {-sys.maxsize - 1 if min_value is None else min_value} {sys.maxsize if max_value is None else max_value}' + @staticmethod + def _metavar(): + return 'value' return IntBounded() def Float(min_value=None, max_value=None): # pylint: disable=invalid-name,no-self-argument @@ -703,8 +712,11 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError(f'Input value "{input_value}" greater than maximum permissible value {max_value}') return value @staticmethod - def _typestring(): + def _legacytypestring(): return f'FLOAT {"-inf" if min_value is None else str(min_value)} {"inf" if max_value is None else str(max_value)}' + @staticmethod + def _metavar(): + return 'value' return FloatBounded() class SequenceInt(CustomTypeBase): @@ -714,8 +726,11 @@ def __call__(self, input_value): except ValueError as exc: raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as integer sequence') from exc @staticmethod - def _typestring(): + def _legacytypestring(): return 'ISEQ' + @staticmethod + def _metavar(): + return 'values' class SequenceFloat(CustomTypeBase): def __call__(self, input_value): @@ -724,8 +739,11 @@ def __call__(self, input_value): except ValueError as exc: raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as floating-point sequence') from exc @staticmethod - def _typestring(): + def _legacytypestring(): return 'FSEQ' + @staticmethod + def _metavar(): + return 'values' class DirectoryIn(CustomTypeBase): def __call__(self, input_value): @@ -736,8 +754,11 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError(f'Input path "{input_value}" is not a directory') return abspath @staticmethod - def _typestring(): + def _legacytypestring(): return 'DIRIN' + @staticmethod + def _metavar(): + return 'directory' class DirectoryOut(CustomTypeBase): @@ -746,8 +767,11 @@ def __call__(self, input_value): abspath = _UserDirOutPath(input_value) return abspath @staticmethod - def _typestring(): + def _legacytypestring(): return 'DIROUT' + @staticmethod + def _metavar(): + return 'directory' class FileIn(CustomTypeBase): def __call__(self, input_value): @@ -758,16 +782,22 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError(f'Input path "{input_value}" is not a file') return abspath @staticmethod - def _typestring(): + def _legacytypestring(): return 'FILEIN' + @staticmethod + def _metavar(): + return 'file' class FileOut(CustomTypeBase): def __call__(self, input_value): abspath = _UserFileOutPath(input_value) return abspath @staticmethod - def _typestring(): + def _legacytypestring(): return 'FILEOUT' + @staticmethod + def _metavar(): + return 'file' class ImageIn(CustomTypeBase): def __call__(self, input_value): @@ -777,8 +807,11 @@ def __call__(self, input_value): abspath = UserPath(input_value) return abspath @staticmethod - def _typestring(): + def _legacytypestring(): return 'IMAGEIN' + @staticmethod + def _metavar(): + return 'image' class ImageOut(CustomTypeBase): def __call__(self, input_value): @@ -790,8 +823,11 @@ def __call__(self, input_value): abspath = _UserFileOutPath(input_value) return abspath @staticmethod - def _typestring(): + def _legacytypestring(): return 'IMAGEOUT' + @staticmethod + def _metavar(): + return 'image' class TracksIn(CustomTypeBase): def __call__(self, input_value): @@ -800,8 +836,11 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError(f'Input tractogram file "{filepath}" is not a valid track file') return filepath @staticmethod - def _typestring(): + def _legacytypestring(): return 'TRACKSIN' + @staticmethod + def _metavar(): + return 'trackfile' class TracksOut(CustomTypeBase): def __call__(self, input_value): @@ -810,15 +849,21 @@ def __call__(self, input_value): raise argparse.ArgumentTypeError(f'Output tractogram path "{filepath}" does not use the requisite ".tck" suffix') return filepath @staticmethod - def _typestring(): + def _legacytypestring(): return 'TRACKSOUT' + @staticmethod + def _metavar(): + return 'trackfile' class Various(CustomTypeBase): def __call__(self, input_value): return input_value @staticmethod - def _typestring(): + def _legacytypestring(): return 'VARIOUS' + @staticmethod + def _metavar(): + return 'spec' @@ -1014,11 +1059,30 @@ def _check_mutex_options(self, args_in): sys.stderr.flush() sys.exit(1) - - - - - + @staticmethod + def _option2metavar(option): + if option.metavar is not None: + if isinstance(option.metavar, tuple): + return f' {" ".join(option.metavar)}' + text = option.metavar + elif option.choices is not None: + return ' choice' + elif isinstance(option.type, Parser.CustomTypeBase): + text = option.type._metavar() + elif isinstance(option.type, str): + text = 'string' + elif isinstance(option.type, (int, float)): + text = 'value' + else: + return '' + if option.nargs: + if isinstance(option.nargs, int): + text = ((f' {text}') * option.nargs).lstrip() + elif option.nargs in ('+', '*'): + text = f'' + elif option.nargs == '?': + text = '' + return f' {text}' def format_usage(self): argument_list = [ ] @@ -1119,26 +1183,7 @@ def print_group_options(group): group_text = '' for option in group._group_actions: group_text += ' ' + underline('/'.join(option.option_strings)) - if option.metavar: - group_text += ' ' - if isinstance(option.metavar, tuple): - group_text += ' '.join(option.metavar) - else: - group_text += option.metavar - elif option.nargs: - if isinstance(option.nargs, int): - group_text += (f' {option.dest.upper()}')*option.nargs - elif option.nargs in ('+', '*'): - group_text += ' ' - elif option.nargs == '?': - group_text += ' ' - elif option.type is not None: - if hasattr(option.type, '__class__'): - group_text += f' {option.type.__class__.__name__.upper()}' - else: - group_text += f' {option.type.__name__.upper()}' - elif option.default is None: - group_text += f' {option.dest.upper()}' + group_text += Parser._option2metavar(option) # Any options that haven't tripped one of the conditions above should be a store_true or store_false, and # therefore there's nothing to be appended to the option instruction if isinstance(option, argparse._AppendAction): @@ -1221,8 +1266,8 @@ def arg2str(arg): if isinstance(arg.type, str) or arg.type is str or arg.type is None: return 'TEXT' if isinstance(arg.type, Parser.CustomTypeBase): - return type(arg.type)._typestring() - return arg.type._typestring() + return type(arg.type)._legacytypestring() + return arg.type._legacytypestring() def allow_multiple(nargs): return '1' if nargs in ('*', '+') else '0' @@ -1294,12 +1339,7 @@ def print_group_options(group): group_text = '' for option in group._group_actions: option_text = '/'.join(option.option_strings) - if option.metavar: - option_text += ' ' - if isinstance(option.metavar, tuple): - option_text += ' '.join(option.metavar) - else: - option_text += option.metavar + optiontext += Parser._option2metavar(option) group_text += f'+ **-{option_text}**' if isinstance(option, argparse._AppendAction): group_text += ' *(multiple uses permitted)*' @@ -1381,12 +1421,7 @@ def print_group_options(group): group_text = '' for option in group._group_actions: option_text = '/'.join(option.option_strings) - if option.metavar: - option_text += ' ' - if isinstance(option.metavar, tuple): - option_text += ' '.join(option.metavar) - else: - option_text += option.metavar + option_text += Parser._option2metavar(option) group_text += '\n' group_text += f'- **{option_text}**' if isinstance(option, argparse._AppendAction): diff --git a/python/lib/mrtrix3/dwi2mask/3dautomask.py b/python/lib/mrtrix3/dwi2mask/3dautomask.py index 58ee6af18e..5f936cbae9 100644 --- a/python/lib/mrtrix3/dwi2mask/3dautomask.py +++ b/python/lib/mrtrix3/dwi2mask/3dautomask.py @@ -48,10 +48,12 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'Add this option to use a fixed clip level.') options.add_argument('-peels', type=app.Parser.Int(0), + metavar='iterations', help='Peel (erode) the mask n times, ' 'then unpeel (dilate).') options.add_argument('-nbhrs', type=app.Parser.Int(6, 26), + metavar='count', help='Define the number of neighbors needed for a voxel NOT to be eroded. ' 'It should be between 6 and 26.') options.add_argument('-eclip', @@ -66,9 +68,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable '130 seems to be decent (i.e., for Homo Sapiens brains).') options.add_argument('-dilate', type=app.Parser.Int(0), + metavar='iterations', help='Dilate the mask outwards n times') options.add_argument('-erode', type=app.Parser.Int(0), + metavar='iterations', help='Erode the mask outwards n times') options.add_argument('-NN1', diff --git a/python/lib/mrtrix3/dwi2mask/consensus.py b/python/lib/mrtrix3/dwi2mask/consensus.py index db9995bcd0..906dfb0e78 100644 --- a/python/lib/mrtrix3/dwi2mask/consensus.py +++ b/python/lib/mrtrix3/dwi2mask/consensus.py @@ -37,7 +37,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'of dwi2mask algorithms that are to be utilised') options.add_argument('-masks', type=app.Parser.ImageOut(), - metavar='image', help='Export a 4D image containing the individual algorithm masks') options.add_argument('-template', type=app.Parser.ImageIn(), diff --git a/python/lib/mrtrix3/dwi2mask/mtnorm.py b/python/lib/mrtrix3/dwi2mask/mtnorm.py index f99a41dc81..1d38bb81d5 100644 --- a/python/lib/mrtrix3/dwi2mask/mtnorm.py +++ b/python/lib/mrtrix3/dwi2mask/mtnorm.py @@ -59,25 +59,21 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-init_mask', type=app.Parser.ImageIn(), - metavar='image', help='Provide an initial brain mask, ' 'which will constrain the response function estimation ' '(if omitted, the default dwi2mask algorithm will be used)') options.add_argument('-lmax', type=app.Parser.SequenceInt(), - metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data') options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), - metavar='value', default=THRESHOLD_DEFAULT, help='the threshold on the total tissue density sum image used to derive the brain mask; ' f'default is {THRESHOLD_DEFAULT}') options.add_argument('-tissuesum', type=app.Parser.ImageOut(), - metavar='image', help='Export the tissue sum image that was used to generate the mask') diff --git a/python/lib/mrtrix3/dwi2mask/synthstrip.py b/python/lib/mrtrix3/dwi2mask/synthstrip.py index 5f54e9d9d9..99e3ee802c 100644 --- a/python/lib/mrtrix3/dwi2mask/synthstrip.py +++ b/python/lib/mrtrix3/dwi2mask/synthstrip.py @@ -50,7 +50,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable help='Use the GPU') options.add_argument('-model', type=app.Parser.FileIn(), - metavar='file', help='Alternative model weights') options.add_argument('-nocsf', action='store_true', diff --git a/python/lib/mrtrix3/dwi2mask/trace.py b/python/lib/mrtrix3/dwi2mask/trace.py index aa760e9dde..0b47727ac1 100644 --- a/python/lib/mrtrix3/dwi2mask/trace.py +++ b/python/lib/mrtrix3/dwi2mask/trace.py @@ -51,6 +51,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'prior to thresholding') iter_options.add_argument('-max_iters', type=app.Parser.Int(1), + metavar='iterations', default=DEFAULT_MAX_ITERS, help='Set the maximum number of iterations for the algorithm ' f'(default: {DEFAULT_MAX_ITERS})') diff --git a/python/lib/mrtrix3/dwi2response/dhollander.py b/python/lib/mrtrix3/dwi2response/dhollander.py index a0b2634d9d..4cc0292ed6 100644 --- a/python/lib/mrtrix3/dwi2response/dhollander.py +++ b/python/lib/mrtrix3/dwi2response/dhollander.py @@ -60,7 +60,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options for the "dhollander" algorithm') options.add_argument('-erode', type=app.Parser.Int(0), - metavar='passes', + metavar='iterations', default=3, help='Number of erosion passes to apply to initial (whole brain) mask. ' 'Set to 0 to not erode the brain mask. ' diff --git a/python/lib/mrtrix3/dwi2response/fa.py b/python/lib/mrtrix3/dwi2response/fa.py index 64dfa17bd6..0ae2cf85f7 100644 --- a/python/lib/mrtrix3/dwi2response/fa.py +++ b/python/lib/mrtrix3/dwi2response/fa.py @@ -38,7 +38,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "fa" algorithm') options.add_argument('-erode', type=app.Parser.Int(0), - metavar='passes', + metavar='iterations', default=3, help='Number of brain mask erosion steps to apply prior to threshold ' '(not used if mask is provided manually)') @@ -49,7 +49,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable help='The number of highest-FA voxels to use') options.add_argument('-threshold', type=app.Parser.Float(0.0, 1.0), - metavar='value', help='Apply a hard FA threshold, ' 'rather than selecting the top voxels') parser.flag_mutually_exclusive_options( [ 'number', 'threshold' ] ) diff --git a/python/lib/mrtrix3/dwi2response/manual.py b/python/lib/mrtrix3/dwi2response/manual.py index 34d12713f2..1a2167a159 100644 --- a/python/lib/mrtrix3/dwi2response/manual.py +++ b/python/lib/mrtrix3/dwi2response/manual.py @@ -38,7 +38,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "manual" algorithm') options.add_argument('-dirs', type=app.Parser.ImageIn(), - metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel ' '(a tensor fit will be used otherwise)') diff --git a/python/lib/mrtrix3/dwi2response/msmt_5tt.py b/python/lib/mrtrix3/dwi2response/msmt_5tt.py index 881aa5b4c0..9706033dbe 100644 --- a/python/lib/mrtrix3/dwi2response/msmt_5tt.py +++ b/python/lib/mrtrix3/dwi2response/msmt_5tt.py @@ -50,12 +50,10 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "msmt_5tt" algorithm') options.add_argument('-dirs', type=app.Parser.ImageIn(), - metavar='image', help='Provide an input image that contains a pre-estimated fibre direction in each voxel ' '(a tensor fit will be used otherwise)') options.add_argument('-fa', type=app.Parser.Float(0.0, 1.0), - metavar='value', default=0.2, help='Upper fractional anisotropy threshold for GM and CSF voxel selection ' '(default: 0.2)') @@ -73,7 +71,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable f'(options: {", ".join(WM_ALGOS)}; default: tournier)') options.add_argument('-sfwm_fa_threshold', type=app.Parser.Float(0.0, 1.0), - metavar='value', help='Sets -wm_algo to fa and allows to specify a hard FA threshold for single-fibre WM voxels, ' 'which is passed to the -threshold option of the fa algorithm ' '(warning: overrides -wm_algo option)') diff --git a/python/lib/mrtrix3/dwi2response/tax.py b/python/lib/mrtrix3/dwi2response/tax.py index 2fb4248517..e3238acfa3 100644 --- a/python/lib/mrtrix3/dwi2response/tax.py +++ b/python/lib/mrtrix3/dwi2response/tax.py @@ -38,7 +38,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "tax" algorithm') options.add_argument('-peak_ratio', type=app.Parser.Float(0.0, 1.0), - metavar='value', default=0.1, help='Second-to-first-peak amplitude ratio threshold') options.add_argument('-max_iters', diff --git a/python/lib/mrtrix3/dwi2response/tournier.py b/python/lib/mrtrix3/dwi2response/tournier.py index 7ef04eb76c..f29ce9bac6 100644 --- a/python/lib/mrtrix3/dwi2response/tournier.py +++ b/python/lib/mrtrix3/dwi2response/tournier.py @@ -49,7 +49,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable '(default = 10 x value given in -number)') options.add_argument('-dilate', type=app.Parser.Int(1), - metavar='passes', + metavar='iterations', default=1, help='Number of mask dilation steps to apply when deriving voxel mask to test in the next iteration') options.add_argument('-max_iters', diff --git a/python/lib/mrtrix3/dwibiascorrect/mtnorm.py b/python/lib/mrtrix3/dwibiascorrect/mtnorm.py index d86125f9bf..502bce0675 100644 --- a/python/lib/mrtrix3/dwibiascorrect/mtnorm.py +++ b/python/lib/mrtrix3/dwibiascorrect/mtnorm.py @@ -57,7 +57,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', type=app.Parser.SequenceInt(), - metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data)') diff --git a/python/lib/mrtrix3/dwinormalise/group.py b/python/lib/mrtrix3/dwinormalise/group.py index 2ecea1d33f..04a62a9aa0 100644 --- a/python/lib/mrtrix3/dwinormalise/group.py +++ b/python/lib/mrtrix3/dwinormalise/group.py @@ -54,7 +54,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable parser.add_argument('-fa_threshold', type=app.Parser.Float(0.0, 1.0), default=FA_THRESHOLD_DEFAULT, - metavar='value', help='The threshold applied to the Fractional Anisotropy group template ' 'used to derive an approximate white matter mask ' f'(default: {FA_THRESHOLD_DEFAULT})') diff --git a/python/lib/mrtrix3/dwinormalise/manual.py b/python/lib/mrtrix3/dwinormalise/manual.py index 845a57a3d1..61e3d021c1 100644 --- a/python/lib/mrtrix3/dwinormalise/manual.py +++ b/python/lib/mrtrix3/dwinormalise/manual.py @@ -37,13 +37,11 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable help='The output intensity-normalised DWI series') parser.add_argument('-intensity', type=app.Parser.Float(0.0), - metavar='value', default=DEFAULT_TARGET_INTENSITY, help='Normalise the b=0 signal to a specified value ' f'(Default: {DEFAULT_TARGET_INTENSITY})') parser.add_argument('-percentile', type=app.Parser.Float(0.0, 100.0), - metavar='value', help='Define the percentile of the b=0 image intensties within the mask used for normalisation; ' 'if this option is not supplied then the median value (50th percentile) ' 'will be normalised to the desired intensity value') diff --git a/python/lib/mrtrix3/dwinormalise/mtnorm.py b/python/lib/mrtrix3/dwinormalise/mtnorm.py index e8e01647b3..279c336f8f 100644 --- a/python/lib/mrtrix3/dwinormalise/mtnorm.py +++ b/python/lib/mrtrix3/dwinormalise/mtnorm.py @@ -62,24 +62,20 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "mtnorm" algorithm') options.add_argument('-lmax', type=app.Parser.SequenceInt(), - metavar='values', help='The maximum spherical harmonic degree for the estimated FODs (see Description); ' f'defaults are "{",".join(map(str, LMAXES_MULTI))}" for multi-shell ' f'and "{",".join(map(str, LMAXES_SINGLE))}" for single-shell data)') options.add_argument('-mask', type=app.Parser.ImageIn(), - metavar='image', help='Provide a mask image for relevant calculations ' '(if not provided, the default dwi2mask algorithm will be used)') options.add_argument('-reference', type=app.Parser.Float(0.0), - metavar='value', default=REFERENCE_INTENSITY, help='Set the target CSF b=0 intensity in the output DWI series ' f'(default: {REFERENCE_INTENSITY})') options.add_argument('-scale', type=app.Parser.FileOut(), - metavar='file', help='Write the scaling factor applied to the DWI series to a text file') app.add_dwgrad_import_options(parser) From d1588917992eeb0c74be972f846e89cf4dca2074 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 28 Feb 2024 20:44:33 +1100 Subject: [PATCH 40/75] clang-format and clang-tidy fixes for #2678 --- cmd/mrregister.cpp | 4 ++-- core/image_io/pipe.cpp | 22 +++++++++++----------- src/registration/linear.cpp | 12 ++++++------ src/registration/linear.h | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/mrregister.cpp b/cmd/mrregister.cpp index 0e6fe948a0..d6b689fd1f 100644 --- a/cmd/mrregister.cpp +++ b/cmd/mrregister.cpp @@ -475,7 +475,7 @@ void run() { rigid_estimator = Registration::None; break; default: - assert (false); + assert(false); } } @@ -622,7 +622,7 @@ void run() { affine_estimator = Registration::None; break; default: - assert (false); + assert(false); } } diff --git a/core/image_io/pipe.cpp b/core/image_io/pipe.cpp index f027d61e23..e7aabee49a 100644 --- a/core/image_io/pipe.cpp +++ b/core/image_io/pipe.cpp @@ -49,19 +49,19 @@ void Pipe::unload(const Header &) { } } -//ENVVAR name: MRTRIX_PRESERVE_TMPFILE -//ENVVAR This variable decides whether the temporary piped image -//ENVVAR should be preserved rather than the usual behaviour of -//ENVVAR deletion at command completion. -//ENVVAR For example, in case of piped commands from Python API, -//ENVVAR it is necessary to retain the temp files until all -//ENVVAR the piped commands are executed. +// ENVVAR name: MRTRIX_PRESERVE_TMPFILE +// ENVVAR This variable decides whether the temporary piped image +// ENVVAR should be preserved rather than the usual behaviour of +// ENVVAR deletion at command completion. +// ENVVAR For example, in case of piped commands from Python API, +// ENVVAR it is necessary to retain the temp files until all +// ENVVAR the piped commands are executed. namespace { - bool preserve_tmpfile() { - const char* const MRTRIX_PRESERVE_TMPFILE = getenv("MRTRIX_PRESERVE_TMPFILE"); - return (MRTRIX_PRESERVE_TMPFILE && to(std::string(MRTRIX_PRESERVE_TMPFILE))); - } +bool preserve_tmpfile() { + const char *const MRTRIX_PRESERVE_TMPFILE = getenv("MRTRIX_PRESERVE_TMPFILE"); + return (MRTRIX_PRESERVE_TMPFILE != nullptr && to(std::string(MRTRIX_PRESERVE_TMPFILE))); } +} // namespace bool Pipe::delete_piped_images = !preserve_tmpfile(); } // namespace MR::ImageIO diff --git a/src/registration/linear.cpp b/src/registration/linear.cpp index 2c7374cce1..6450ff7452 100644 --- a/src/registration/linear.cpp +++ b/src/registration/linear.cpp @@ -20,13 +20,13 @@ namespace MR::Registration { using namespace App; -const char *initialisation_translation_choices[] = {"mass", "geometric", "none", nullptr}; -const char *initialisation_rotation_choices[] = {"search", "moments", "none", nullptr}; +const char *const initialisation_translation_choices[] = {"mass", "geometric", "none", nullptr}; +const char *const initialisation_rotation_choices[] = {"search", "moments", "none", nullptr}; -const char *linear_metric_choices[] = {"diff", "ncc", nullptr}; -const char* linear_robust_estimator_choices[] = { "l1", "l2", "lp", "none", nullptr }; -const char* linear_optimisation_algo_choices[] = { "bbgd", "gd", nullptr }; -const char *optim_algo_names[] = {"BBGD", "GD", nullptr}; +const char *const linear_metric_choices[] = {"diff", "ncc", nullptr}; +const char *const linear_robust_estimator_choices[] = {"l1", "l2", "lp", "none", nullptr}; +const char *const linear_optimisation_algo_choices[] = {"bbgd", "gd", nullptr}; +const char *const optim_algo_names[] = {"BBGD", "GD", nullptr}; // define parameters of initialisation methods used for both, rigid and affine registration void parse_general_options(Registration::Linear ®istration) { diff --git a/src/registration/linear.h b/src/registration/linear.h index f5fd066142..f553474f16 100644 --- a/src/registration/linear.h +++ b/src/registration/linear.h @@ -50,7 +50,7 @@ extern const App::OptionGroup lin_stage_options; extern const App::OptionGroup rigid_options; extern const App::OptionGroup affine_options; extern const App::OptionGroup fod_options; -extern const char *optim_algo_names[]; +extern const char *const optim_algo_names[]; enum LinearMetricType { Diff, NCC }; enum LinearRobustMetricEstimatorType { L1, L2, LP, None }; From d5a4596695a16a160e91d7293a9f6f751b8c1f8b Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 28 Feb 2024 20:56:41 +1100 Subject: [PATCH 41/75] Add unit testing of Python CLI Note that commit b31c9d9631a6dbdadd9ac7a7e3a00e395c18141d was incomplete in this regard, as it added a test suite but not the executable Python script that it invoked. --- docs/reference/commands/dwi2mask.rst | 2 +- python/lib/mrtrix3/app.py | 25 +-- testing/bin/testing_python_cli | 138 ++++++++++++++ testing/data/python_cli/full_usage.txt | 124 ++++++++++++ testing/data/python_cli/help.txt | 177 ++++++++++++++++++ testing/data/python_cli/markdown.md | 127 +++++++++++++ testing/data/python_cli/restructured_text.rst | 146 +++++++++++++++ testing/tests/python_cli | 6 +- 8 files changed, 732 insertions(+), 13 deletions(-) create mode 100755 testing/bin/testing_python_cli create mode 100644 testing/data/python_cli/full_usage.txt create mode 100644 testing/data/python_cli/help.txt create mode 100644 testing/data/python_cli/markdown.md create mode 100644 testing/data/python_cli/restructured_text.rst diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index 4e73a7fc73..d4ca637b07 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -431,7 +431,7 @@ Options Options specific to the "consensus" algorithm ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-algorithms** Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised +- **-algorithms str ** Provide a (space- or comma-separated) list of dwi2mask algorithms that are to be utilised - **-masks image** Export a 4D image containing the individual algorithm masks diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 6d34066c98..7bfd6ec358 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -762,7 +762,6 @@ def _metavar(): class DirectoryOut(CustomTypeBase): - def __call__(self, input_value): abspath = _UserDirOutPath(input_value) return abspath @@ -1069,19 +1068,21 @@ def _option2metavar(option): return ' choice' elif isinstance(option.type, Parser.CustomTypeBase): text = option.type._metavar() - elif isinstance(option.type, str): - text = 'string' - elif isinstance(option.type, (int, float)): - text = 'value' - else: + elif option.type is not None: + text = option.type.__name__.lower() + elif option.nargs == 0: return '' + else: + text = 'string' if option.nargs: - if isinstance(option.nargs, int): + if isinstance(option.nargs, int) and option.nargs > 1: text = ((f' {text}') * option.nargs).lstrip() - elif option.nargs in ('+', '*'): + elif option.nargs == '*': text = f'' + elif option.nargs == '+': + text = f'{text} ' elif option.nargs == '?': - text = '' + text = f'' return f' {text}' def format_usage(self): @@ -1283,13 +1284,15 @@ def print_group_options(group): for option in group._group_actions: sys.stdout.write(f'OPTION {"/".join(option.option_strings)} {"0" if option.required else "1"} {allow_multiple(option.nargs)}\n') sys.stdout.write(f'{option.help}\n') + if option.nargs == 0: + continue if option.metavar and isinstance(option.metavar, tuple): assert len(option.metavar) == option.nargs for arg in option.metavar: sys.stdout.write(f'ARGUMENT {arg} 0 0 {arg2str(option)}\n') else: multiple = allow_multiple(option.nargs) - nargs = 1 if multiple == '1' else (option.nargs if option.nargs is not None else 1) + nargs = 1 if multiple == '1' else (option.nargs if isinstance(option.nargs, int) else 1) for _ in range(0, nargs): metavar_string = option.metavar if option.metavar else '/'.join(opt.lstrip('-') for opt in option.option_strings) sys.stdout.write(f'ARGUMENT {metavar_string} 0 {multiple} {arg2str(option)}\n') @@ -1339,7 +1342,7 @@ def print_group_options(group): group_text = '' for option in group._group_actions: option_text = '/'.join(option.option_strings) - optiontext += Parser._option2metavar(option) + option_text += Parser._option2metavar(option) group_text += f'+ **-{option_text}**' if isinstance(option, argparse._AppendAction): group_text += ' *(multiple uses permitted)*' diff --git a/testing/bin/testing_python_cli b/testing/bin/testing_python_cli new file mode 100755 index 0000000000..29390ddaf8 --- /dev/null +++ b/testing/bin/testing_python_cli @@ -0,0 +1,138 @@ +#!/usr/bin/python3 + +# Copyright (c) 2008-2024 the MRtrix3 contributors. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Covered Software is provided under this License on an "as is" +# basis, without warranty of any kind, either expressed, implied, or +# statutory, including, without limitation, warranties that the +# Covered Software is free of defects, merchantable, fit for a +# particular purpose or non-infringing. +# See the Mozilla Public License v. 2.0 for more details. +# +# For more details, see http://www.mrtrix.org/. + +CHOICES = ('One', 'Two', 'Three') + +def usage(cmdline): #pylint: disable=unused-variable + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel + + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') + cmdline.set_synopsis('Test operation of the Python command-line interface') + + builtins = cmdline.add_argument_group('Built-in types') + builtins.add_argument('-flag', + action='store_true', + help='A binary flag') + builtins.add_argument('-string_implicit', + help='A built-in string (implicit)') + builtins.add_argument('-string_explicit', + type=str, + help='A built-in string (explicit)') + builtins.add_argument('-choice', + choices=CHOICES, + help='A selection of choices; one of: ' + ', '.join(CHOICES)) + builtins.add_argument('-int_builtin', + type=int, + help='An integer; built-in type') + builtins.add_argument('-float_builtin', + type=float, + help='A floating-point; built-in type') + + complex = cmdline.add_argument_group('Complex interfaces; nargs, metavar, etc.') + complex.add_argument('-nargs_plus', + nargs='+', + help='A command-line option with nargs="+", no metavar') + complex.add_argument('-nargs_asterisk', + nargs='*', + help='A command-line option with nargs="*", no metavar') + complex.add_argument('-nargs_question', + nargs='?', + help='A command-line option with nargs="?", no metavar') + complex.add_argument('-nargs_two', + nargs=2, + help='A command-line option with nargs=2, no metavar') + complex.add_argument('-metavar_one', + metavar='metavar', + help='A command-line option with nargs=1 and metavar="metavar"') + complex.add_argument('-metavar_two', + metavar='metavar', + nargs=2, + help='A command-line option with nargs=2 and metavar="metavar"') + complex.add_argument('-metavar_tuple', + metavar=('metavar_one', 'metavar_two'), + nargs=2, + help='A command-line option with nargs=2 and metavar=("metavar_one", "metavar_two")') + complex.add_argument('-append', + action='append', + help='A command-line option with "append" action (can be specified multiple times)') + + custom = cmdline.add_argument_group('Custom types') + custom.add_argument('-bool', + type=app.Parser.Bool(), + help='A boolean input') + custom.add_argument('-int_unbound', + type=app.Parser.Int(), + help='An integer; unbounded') + custom.add_argument('-int_nonnegative', + type=app.Parser.Int(0), + help='An integer; non-negative') + custom.add_argument('-int_bounded', + type=app.Parser.Int(0, 100), + help='An integer; bound range') + custom.add_argument('-float_unbound', + type=app.Parser.Float(), + help='A floating-point; unbounded') + custom.add_argument('-float_nonneg', + type=app.Parser.Float(0.0), + help='A floating-point; non-negative') + custom.add_argument('-float_bounded', + type=app.Parser.Float(0.0, 1.0), + help='A floating-point; bound range') + custom.add_argument('-int_seq', + type=app.Parser.SequenceInt(), + help='A comma-separated list of integers') + custom.add_argument('-float_seq', + type=app.Parser.SequenceFloat(), + help='A comma-separated list of floating-points') + custom.add_argument('-dir_in', + type=app.Parser.DirectoryIn(), + help='An input directory') + custom.add_argument('-dir_out', + type=app.Parser.DirectoryOut(), + help='An output directory') + custom.add_argument('-file_in', + type=app.Parser.FileIn(), + help='An input file') + custom.add_argument('-file_out', + type=app.Parser.FileOut(), + help='An output file') + custom.add_argument('-image_in', + type=app.Parser.ImageIn(), + help='An input image') + custom.add_argument('-image_out', + type=app.Parser.ImageOut(), + help='An output image') + custom.add_argument('-tracks_in', + type=app.Parser.TracksIn(), + help='An input tractogram') + custom.add_argument('-tracks_out', + type=app.Parser.TracksOut(), + help='An output tractogram') + + + +def execute(): #pylint: disable=unused-variable + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel + + for key, value in app.ARGS.iteritems(): + app.console(f'{key}: {value}') + + + +# Execute the script +import mrtrix3 #pylint: disable=wrong-import-position +mrtrix3.execute() #pylint: disable=no-member diff --git a/testing/data/python_cli/full_usage.txt b/testing/data/python_cli/full_usage.txt new file mode 100644 index 0000000000..04821c67b1 --- /dev/null +++ b/testing/data/python_cli/full_usage.txt @@ -0,0 +1,124 @@ +Test operation of the Python command-line interface +OPTION -bool 1 0 +A boolean input +ARGUMENT bool 0 0 BOOL +OPTION -int_unbound 1 0 +An integer; unbounded +ARGUMENT int_unbound 0 0 INT -9223372036854775808 9223372036854775807 +OPTION -int_nonnegative 1 0 +An integer; non-negative +ARGUMENT int_nonnegative 0 0 INT 0 9223372036854775807 +OPTION -int_bounded 1 0 +An integer; bound range +ARGUMENT int_bounded 0 0 INT 0 100 +OPTION -float_unbound 1 0 +A floating-point; unbounded +ARGUMENT float_unbound 0 0 FLOAT -inf inf +OPTION -float_nonneg 1 0 +A floating-point; non-negative +ARGUMENT float_nonneg 0 0 FLOAT 0.0 inf +OPTION -float_bounded 1 0 +A floating-point; bound range +ARGUMENT float_bounded 0 0 FLOAT 0.0 1.0 +OPTION -int_seq 1 0 +A comma-separated list of integers +ARGUMENT int_seq 0 0 ISEQ +OPTION -float_seq 1 0 +A comma-separated list of floating-points +ARGUMENT float_seq 0 0 FSEQ +OPTION -dir_in 1 0 +An input directory +ARGUMENT dir_in 0 0 DIRIN +OPTION -dir_out 1 0 +An output directory +ARGUMENT dir_out 0 0 DIROUT +OPTION -file_in 1 0 +An input file +ARGUMENT file_in 0 0 FILEIN +OPTION -file_out 1 0 +An output file +ARGUMENT file_out 0 0 FILEOUT +OPTION -image_in 1 0 +An input image +ARGUMENT image_in 0 0 IMAGEIN +OPTION -image_out 1 0 +An output image +ARGUMENT image_out 0 0 IMAGEOUT +OPTION -tracks_in 1 0 +An input tractogram +ARGUMENT tracks_in 0 0 TRACKSIN +OPTION -tracks_out 1 0 +An output tractogram +ARGUMENT tracks_out 0 0 TRACKSOUT +OPTION -nargs_plus 1 1 +A command-line option with nargs="+", no metavar +ARGUMENT nargs_plus 0 1 TEXT +OPTION -nargs_asterisk 1 1 +A command-line option with nargs="*", no metavar +ARGUMENT nargs_asterisk 0 1 TEXT +OPTION -nargs_question 1 0 +A command-line option with nargs="?", no metavar +ARGUMENT nargs_question 0 0 TEXT +OPTION -nargs_two 1 0 +A command-line option with nargs=2, no metavar +ARGUMENT nargs_two 0 0 TEXT +ARGUMENT nargs_two 0 0 TEXT +OPTION -metavar_one 1 0 +A command-line option with nargs=1 and metavar="metavar" +ARGUMENT metavar 0 0 TEXT +OPTION -metavar_two 1 0 +A command-line option with nargs=2 and metavar="metavar" +ARGUMENT metavar 0 0 TEXT +ARGUMENT metavar 0 0 TEXT +OPTION -metavar_tuple 1 0 +A command-line option with nargs=2 and metavar=("metavar_one", "metavar_two") +ARGUMENT metavar_one 0 0 TEXT +ARGUMENT metavar_two 0 0 TEXT +OPTION -append 1 0 +A command-line option with "append" action (can be specified multiple times) +ARGUMENT append 0 0 TEXT +OPTION -flag 1 0 +A binary flag +OPTION -string_implicit 1 0 +A built-in string (implicit) +ARGUMENT string_implicit 0 0 TEXT +OPTION -string_explicit 1 0 +A built-in string (explicit) +ARGUMENT string_explicit 0 0 TEXT +OPTION -choice 1 0 +A selection of choices; one of: One, Two, Three +ARGUMENT choice 0 0 CHOICE One Two Three +OPTION -int_builtin 1 0 +An integer; built-in type +ARGUMENT int_builtin 0 0 INT -9223372036854775808 9223372036854775807 +OPTION -float_builtin 1 0 +A floating-point; built-in type +ARGUMENT float_builtin 0 0 FLOAT -inf inf +OPTION -nocleanup 1 0 +do not delete intermediate files during script execution, and do not delete scratch directory at script completion. +OPTION -scratch 1 0 +manually specify the path in which to generate the scratch directory. +ARGUMENT /path/to/scratch/ 0 0 DIROUT +OPTION -continue 1 0 +continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. +ARGUMENT ScratchDir 0 0 VARIOUS +ARGUMENT LastFile 0 0 VARIOUS +OPTION -info 1 0 +display information messages. +OPTION -quiet 1 0 +do not display information messages or progress status. Alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string. +OPTION -debug 1 0 +display debugging messages. +OPTION -force 1 0 +force overwrite of output files. +OPTION -nthreads 1 0 +use this number of threads in multi-threaded applications (set to 0 to disable multi-threading). +ARGUMENT number 0 0 INT 0 9223372036854775807 +OPTION -config 1 0 +temporarily set the value of an MRtrix config file entry. +ARGUMENT key 0 0 TEXT +ARGUMENT value 0 0 TEXT +OPTION -help 1 0 +display this information page and exit. +OPTION -version 1 0 +display version information and exit. diff --git a/testing/data/python_cli/help.txt b/testing/data/python_cli/help.txt new file mode 100644 index 0000000000..84e1304835 --- /dev/null +++ b/testing/data/python_cli/help.txt @@ -0,0 +1,177 @@ +Version 3.0.4-762-gd1588917-dirty tteessttiinngg__ppyytthhoonn__ccllii +using MRtrix3 3.0.4-605-g24f1c322-dirty + + tteessttiinngg__ppyytthhoonn__ccllii: external MRtrix3 project + +SSYYNNOOPPSSIISS + + Test operation of the Python command-line interface + +UUSSAAGGEE + + _t_e_s_t_i_n_g___p_y_t_h_o_n___c_l_i [ options ] + +CCuussttoomm  ttyyppeess + + _-_b_o_o_l value + A boolean input + + _-_i_n_t___u_n_b_o_u_n_d value + An integer; unbounded + + _-_i_n_t___n_o_n_n_e_g_a_t_i_v_e value + An integer; non-negative + + _-_i_n_t___b_o_u_n_d_e_d value + An integer; bound range + + _-_f_l_o_a_t___u_n_b_o_u_n_d value + A floating-point; unbounded + + _-_f_l_o_a_t___n_o_n_n_e_g value + A floating-point; non-negative + + _-_f_l_o_a_t___b_o_u_n_d_e_d value + A floating-point; bound range + + _-_i_n_t___s_e_q values + A comma-separated list of integers + + _-_f_l_o_a_t___s_e_q values + A comma-separated list of floating-points + + _-_d_i_r___i_n directory + An input directory + + _-_d_i_r___o_u_t directory + An output directory + + _-_f_i_l_e___i_n file + An input file + + _-_f_i_l_e___o_u_t file + An output file + + _-_i_m_a_g_e___i_n image + An input image + + _-_i_m_a_g_e___o_u_t image + An output image + + _-_t_r_a_c_k_s___i_n trackfile + An input tractogram + + _-_t_r_a_c_k_s___o_u_t trackfile + An output tractogram + +CCoommpplleexx  iinntteerrffaacceess;;  nnaarrggss,,  mmeettaavvaarr,,  eettcc.. + + _-_n_a_r_g_s___p_l_u_s string + A command-line option with nargs="+", no metavar + + _-_n_a_r_g_s___a_s_t_e_r_i_s_k + A command-line option with nargs="*", no metavar + + _-_n_a_r_g_s___q_u_e_s_t_i_o_n + A command-line option with nargs="?", no metavar + + _-_n_a_r_g_s___t_w_o string string + A command-line option with nargs=2, no metavar + + _-_m_e_t_a_v_a_r___o_n_e metavar + A command-line option with nargs=1 and metavar="metavar" + + _-_m_e_t_a_v_a_r___t_w_o metavar metavar + A command-line option with nargs=2 and metavar="metavar" + + _-_m_e_t_a_v_a_r___t_u_p_l_e metavar_one metavar_two + A command-line option with nargs=2 and metavar=("metavar_one", + "metavar_two") + + _-_a_p_p_e_n_d string (multiple uses permitted) + A command-line option with "append" action (can be specified multiple + times) + +BBuuiilltt--iinn  ttyyppeess + + _-_f_l_a_g + A binary flag + + _-_s_t_r_i_n_g___i_m_p_l_i_c_i_t string + A built-in string (implicit) + + _-_s_t_r_i_n_g___e_x_p_l_i_c_i_t str + A built-in string (explicit) + + _-_c_h_o_i_c_e choice + A selection of choices; one of: One, Two, Three + + _-_i_n_t___b_u_i_l_t_i_n int + An integer; built-in type + + _-_f_l_o_a_t___b_u_i_l_t_i_n float + A floating-point; built-in type + +AAddddiittiioonnaall  ssttaannddaarrdd  ooppttiioonnss  ffoorr  PPyytthhoonn  ssccrriippttss + + _-_n_o_c_l_e_a_n_u_p + do not delete intermediate files during script execution, and do not delete + scratch directory at script completion. + + _-_s_c_r_a_t_c_h /path/to/scratch/ + manually specify the path in which to generate the scratch directory. + + _-_c_o_n_t_i_n_u_e ScratchDir LastFile + continue the script from a previous execution; must provide the scratch + directory path, and the name of the last successfully-generated file. + +SSttaannddaarrdd  ooppttiioonnss + + _-_i_n_f_o + display information messages. + + _-_q_u_i_e_t + do not display information messages or progress status. Alternatively, this + can be achieved by setting the MRTRIX_QUIET environment variable to a non- + empty string. + + _-_d_e_b_u_g + display debugging messages. + + _-_f_o_r_c_e + force overwrite of output files. + + _-_n_t_h_r_e_a_d_s number + use this number of threads in multi-threaded applications (set to 0 to + disable multi-threading). + + _-_c_o_n_f_i_g key value (multiple uses permitted) + temporarily set the value of an MRtrix config file entry. + + _-_h_e_l_p + display this information page and exit. + + _-_v_e_r_s_i_o_n + display version information and exit. + +AAUUTTHHOORR + Robert E. Smith (robert.smith@florey.edu.au) + +CCOOPPYYRRIIGGHHTT + Copyright (c) 2008-2024 the MRtrix3 contributors. This Source Code Form is + subject to the terms of the Mozilla Public License, v. 2.0. If a copy of + the MPL was not distributed with this file, You can obtain one at + http://mozilla.org/MPL/2.0/. Covered Software is provided under this + License on an "as is" basis, without warranty of any kind, either + expressed, implied, or statutory, including, without limitation, warranties + that the Covered Software is free of defects, merchantable, fit for a + particular purpose or non-infringing. See the Mozilla Public License v. 2.0 + for more details. For more details, see http://www.mrtrix.org/. + +RREEFFEERREENNCCEESS + + Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; + Pietsch, M.; Christiaens, D.; Jeurissen, B.; Yeh, C.-H. & Connelly, A. + MRtrix3: A fast, flexible and open software framework for medical image + processing and visualisation. NeuroImage, 2019, 202, 116137 + diff --git a/testing/data/python_cli/markdown.md b/testing/data/python_cli/markdown.md new file mode 100644 index 0000000000..1d7cb1b0b5 --- /dev/null +++ b/testing/data/python_cli/markdown.md @@ -0,0 +1,127 @@ +## Synopsis + +Test operation of the Python command-line interface + +## Usage + + testing_python_cli [ options ] + +## Options + +#### Custom types + ++ **--bool value**
A boolean input + ++ **--int_unbound value**
An integer; unbounded + ++ **--int_nonnegative value**
An integer; non-negative + ++ **--int_bounded value**
An integer; bound range + ++ **--float_unbound value**
A floating-point; unbounded + ++ **--float_nonneg value**
A floating-point; non-negative + ++ **--float_bounded value**
A floating-point; bound range + ++ **--int_seq values**
A comma-separated list of integers + ++ **--float_seq values**
A comma-separated list of floating-points + ++ **--dir_in directory**
An input directory + ++ **--dir_out directory**
An output directory + ++ **--file_in file**
An input file + ++ **--file_out file**
An output file + ++ **--image_in image**
An input image + ++ **--image_out image**
An output image + ++ **--tracks_in trackfile**
An input tractogram + ++ **--tracks_out trackfile**
An output tractogram + +#### Complex interfaces; nargs, metavar, etc. + ++ **--nargs_plus string **
A command-line option with nargs="+", no metavar + ++ **--nargs_asterisk **
A command-line option with nargs="*", no metavar + ++ **--nargs_question **
A command-line option with nargs="?", no metavar + ++ **--nargs_two string string**
A command-line option with nargs=2, no metavar + ++ **--metavar_one metavar**
A command-line option with nargs=1 and metavar="metavar" + ++ **--metavar_two metavar metavar**
A command-line option with nargs=2 and metavar="metavar" + ++ **--metavar_tuple metavar_one metavar_two**
A command-line option with nargs=2 and metavar=("metavar_one", "metavar_two") + ++ **--append string** *(multiple uses permitted)*
A command-line option with "append" action (can be specified multiple times) + +#### Built-in types + ++ **--flag**
A binary flag + ++ **--string_implicit string**
A built-in string (implicit) + ++ **--string_explicit str**
A built-in string (explicit) + ++ **--choice choice**
A selection of choices; one of: One, Two, Three + ++ **--int_builtin int**
An integer; built-in type + ++ **--float_builtin float**
A floating-point; built-in type + +#### Additional standard options for Python scripts + ++ **--nocleanup**
do not delete intermediate files during script execution, and do not delete scratch directory at script completion. + ++ **--scratch /path/to/scratch/**
manually specify the path in which to generate the scratch directory. + ++ **--continue ScratchDir LastFile**
continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. + +#### Standard options + ++ **--info**
display information messages. + ++ **--quiet**
do not display information messages or progress status. Alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string. + ++ **--debug**
display debugging messages. + ++ **--force**
force overwrite of output files. + ++ **--nthreads number**
use this number of threads in multi-threaded applications (set to 0 to disable multi-threading). + ++ **--config key value** *(multiple uses permitted)*
temporarily set the value of an MRtrix config file entry. + ++ **--help**
display this information page and exit. + ++ **--version**
display version information and exit. + +## References + +Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; Pietsch, M.; Christiaens, D.; Jeurissen, B.; Yeh, C.-H. & Connelly, A. MRtrix3: A fast, flexible and open software framework for medical image processing and visualisation. NeuroImage, 2019, 202, 116137 + +--- + +**Author:** Robert E. Smith (robert.smith@florey.edu.au) + +**Copyright:** Copyright (c) 2008-2024 the MRtrix3 contributors. + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Covered Software is provided under this License on an "as is" +basis, without warranty of any kind, either expressed, implied, or +statutory, including, without limitation, warranties that the +Covered Software is free of defects, merchantable, fit for a +particular purpose or non-infringing. +See the Mozilla Public License v. 2.0 for more details. + +For more details, see http://www.mrtrix.org/. + diff --git a/testing/data/python_cli/restructured_text.rst b/testing/data/python_cli/restructured_text.rst new file mode 100644 index 0000000000..a5093b98e5 --- /dev/null +++ b/testing/data/python_cli/restructured_text.rst @@ -0,0 +1,146 @@ +.. _testing_python_cli: + +testing_python_cli +================== + +Synopsis +-------- + +Test operation of the Python command-line interface + +Usage +----- + +:: + + testing_python_cli [ options ] + + +Options +------- + +Custom types +^^^^^^^^^^^^ + +- **-bool value** A boolean input + +- **-int_unbound value** An integer; unbounded + +- **-int_nonnegative value** An integer; non-negative + +- **-int_bounded value** An integer; bound range + +- **-float_unbound value** A floating-point; unbounded + +- **-float_nonneg value** A floating-point; non-negative + +- **-float_bounded value** A floating-point; bound range + +- **-int_seq values** A comma-separated list of integers + +- **-float_seq values** A comma-separated list of floating-points + +- **-dir_in directory** An input directory + +- **-dir_out directory** An output directory + +- **-file_in file** An input file + +- **-file_out file** An output file + +- **-image_in image** An input image + +- **-image_out image** An output image + +- **-tracks_in trackfile** An input tractogram + +- **-tracks_out trackfile** An output tractogram + +Complex interfaces; nargs, metavar, etc. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **-nargs_plus string ** A command-line option with nargs="+", no metavar + +- **-nargs_asterisk ** A command-line option with nargs="*", no metavar + +- **-nargs_question ** A command-line option with nargs="?", no metavar + +- **-nargs_two string string** A command-line option with nargs=2, no metavar + +- **-metavar_one metavar** A command-line option with nargs=1 and metavar="metavar" + +- **-metavar_two metavar metavar** A command-line option with nargs=2 and metavar="metavar" + +- **-metavar_tuple metavar_one metavar_two** A command-line option with nargs=2 and metavar=("metavar_one", "metavar_two") + +- **-append string** *(multiple uses permitted)* A command-line option with "append" action (can be specified multiple times) + +Built-in types +^^^^^^^^^^^^^^ + +- **-flag** A binary flag + +- **-string_implicit string** A built-in string (implicit) + +- **-string_explicit str** A built-in string (explicit) + +- **-choice choice** A selection of choices; one of: One, Two, Three + +- **-int_builtin int** An integer; built-in type + +- **-float_builtin float** A floating-point; built-in type + +Additional standard options for Python scripts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. + +- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. + +- **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. + +Standard options +^^^^^^^^^^^^^^^^ + +- **-info** display information messages. + +- **-quiet** do not display information messages or progress status. Alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string. + +- **-debug** display debugging messages. + +- **-force** force overwrite of output files. + +- **-nthreads number** use this number of threads in multi-threaded applications (set to 0 to disable multi-threading). + +- **-config key value** *(multiple uses permitted)* temporarily set the value of an MRtrix config file entry. + +- **-help** display this information page and exit. + +- **-version** display version information and exit. + +References +^^^^^^^^^^ + +Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; Pietsch, M.; Christiaens, D.; Jeurissen, B.; Yeh, C.-H. & Connelly, A. MRtrix3: A fast, flexible and open software framework for medical image processing and visualisation. NeuroImage, 2019, 202, 116137 + +-------------- + + + +**Author:** Robert E. Smith (robert.smith@florey.edu.au) + +**Copyright:** Copyright (c) 2008-2024 the MRtrix3 contributors. + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Covered Software is provided under this License on an "as is" +basis, without warranty of any kind, either expressed, implied, or +statutory, including, without limitation, warranties that the +Covered Software is free of defects, merchantable, fit for a +particular purpose or non-infringing. +See the Mozilla Public License v. 2.0 for more details. + +For more details, see http://www.mrtrix.org/. + diff --git a/testing/tests/python_cli b/testing/tests/python_cli index eccf20d1d8..d2152510d2 100644 --- a/testing/tests/python_cli +++ b/testing/tests/python_cli @@ -1,4 +1,8 @@ -mkdir -p tmp-newdirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -untyped my_untyped -string my_string -bool false -int_builtin 0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_builtin 0.0 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -intseq 1,2,3 -floatseq 0.1,0.2,0.3 -dirin tmp-dirin/ -dirout tmp-dirout/ -filein tmp-filein.txt -fileout tmp-fileout.txt -tracksin tmp-tracksin.tck -tracksout tmp-tracksout.tck -various my_various +mkdir -p tmp-newdirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -intseq 1,2,3 -floatseq 0.1,0.2,0.3 -dirin tmp-dirin/ -dirout tmp-dirout/ -filein tmp-filein.txt -fileout tmp-fileout.txt -tracksin tmp-tracksin.tck -tracksout tmp-tracksout.tck -various my_various +testing_python_cli -help > tmp.txt && diff -q tmp.txt data/python_cli/help.txt +testing_python_cli __print_full_usage__ > tmp.txt && diff -q tmp.txt data/python_cli/full_usage.txt +testing_python_cli __print_usage_markdown__ > tmp.md && diff -q tmp.md data/python_cli/markdown.md +testing_python_cli __print_usage_rst__ > tmp.rst && diff -q tmp.rst data/python_cli/restructured_text.rst testing_python_cli -bool false testing_python_cli -bool False testing_python_cli -bool FALSE From 9cee5ae348b307647d46ebb94db9d772c2f32e4f Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 29 Feb 2024 10:27:37 +1100 Subject: [PATCH 42/75] dwicat: Use F-strings Updates chages in #2702 to fit with #2678. --- python/bin/dwicat | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/python/bin/dwicat b/python/bin/dwicat index 898e4c2a15..6612e53d41 100755 --- a/python/bin/dwicat +++ b/python/bin/dwicat @@ -65,7 +65,6 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable - # TODO Update to use F-strings after merge from mrtrix3 import CONFIG, MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel from mrtrix3 import app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel @@ -166,7 +165,7 @@ def execute(): #pylint: disable=unused-variable else: run.function(shutil.copyfile, infile, outfile) - mask_option = ' -mask_input mask.mif -mask_target mask.mif' if app.ARGS.mask else '' + mask_option = ['-mask_input', 'mask.mif', '-mask_target', 'mask.mif'] if app.ARGS.mask else [] # for all but the first image series: # - find multiplicative factor to match b=0 images to those of the first image @@ -180,7 +179,8 @@ def execute(): #pylint: disable=unused-variable # based on the resulting matrix of optimal scaling factors rescaled_filelist = [ '0in.mif' ] for index in range(1, num_inputs): - stderr_text = run.command('mrhistmatch scale ' + str(index) + 'b0.mif 0b0.mif ' + str(index) + 'rescaledb0.mif' + mask_option).stderr + stderr_text = run.command(['mrhistmatch', 'scale', f'{index}b0.mif', '0b0.mif', f'{index}rescaledb0.mif'] + + mask_option).stderr scaling_factor = None for line in stderr_text.splitlines(): if 'Estimated scale factor is' in line: @@ -191,11 +191,11 @@ def execute(): #pylint: disable=unused-variable break if scaling_factor is None: raise MRtrixError('Unable to extract scaling factor from mrhistmatch output') - filename = str(index) + 'rescaled.mif' - run.command('mrcalc ' + str(index) + 'in.mif ' + str(scaling_factor) + ' -mult ' + filename) + filename = f'{index}rescaled.mif' + run.command(f'mrcalc {index}in.mif {scaling_factor} -mult {filename}') rescaled_filelist.append(filename) else: - rescaled_filelist = [str(index) + 'in.mif' for index in range(0, len(app.ARGS.inputs))] + rescaled_filelist = [f'{index}in.mif' for index in range(0, len(app.ARGS.inputs))] if voxel_grid_matching == 'equal': transformed_filelist = rescaled_filelist @@ -204,13 +204,13 @@ def execute(): #pylint: disable=unused-variable '-spacing', ('mean_nearest' if voxel_grid_matching == 'rigidbody' else 'min_nearest')]) transformed_filelist = [] for item in rescaled_filelist: - transformname = os.path.splitext(item)[0] + '_to_avgheader.txt' - imagename = os.path.splitext(item)[0] + '_transformed.mif' + transformname = f'{os.path.splitext(item)[0]}_to_avgheader.txt' + imagename = f'{os.path.splitext(item)[0]}_transformed.mif' run.command(['transformcalc', item, 'average_header.mif', 'header', transformname]) # Ensure that no non-rigid distortions of image data are applied # in the process of moving it toward the average header if voxel_grid_matching == 'inequal': - newtransformname = os.path.splitext(transformname)[0] + '_rigid.txt' + newtransformname = f'{os.path.splitext(transformname)[0]}_rigid.txt' run.command(['transformcalc', transformname, 'rigid', newtransformname]) transformname = newtransformname run.command(['mrtransform', item, imagename] \ From d783f8c4a476a4288c3e8ba1d68d4cb7ff8708f2 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 29 Feb 2024 10:36:02 +1100 Subject: [PATCH 43/75] population_template: Resolve merge conflicts Resolutions erroneously omitted from 4c331e21b407f555dc5f7ad223d503c27a939cf1. --- python/bin/population_template | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/python/bin/population_template b/python/bin/population_template index 16ffc426e7..46064ed8c9 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -1290,14 +1290,10 @@ def execute(): #pylint: disable=unused-variable f'{inp.msk_transformed}_translated.mif {datatype_option}') progress.increment() # update average space of first contrast to new extent, delete other average space images -<<<<<<< HEAD - run.command(['mraverageheader', [f'{inp.ims_transformed[cid]}_translated.mif' for inp in ins], 'average_header_tight.mif']) -======= run.command(['mraverageheader', - [inp.ims_transformed[cid] + '_translated.mif' for inp in ins], + [f'{inp.ims_transformed[cid]}_translated.mif' for inp in ins], 'average_header_tight.mif', '-spacing', 'mean_nearest']) ->>>>>>> origin/dev progress.done() if voxel_size is None: @@ -1394,15 +1390,9 @@ def execute(): #pylint: disable=unused-variable initialise_option = f' -rigid_init_matrix {init_transform_path}' if do_fod_registration: -<<<<<<< HEAD lmax_option = f' -rigid_lmax {lmax}' if linear_estimator is not None: metric_option = f' -rigid_metric.diff.estimator {linear_estimator}' -======= - lmax_option = ' -rigid_lmax ' + str(lmax) - if linear_estimator is not None: - metric_option = ' -rigid_metric.diff.estimator ' + linear_estimator ->>>>>>> origin/dev if app.VERBOSITY >= 2: mrregister_log_option = f' -info -rigid_log {linear_log_path}' else: @@ -1413,15 +1403,9 @@ def execute(): #pylint: disable=unused-variable contrast_weight_option = cns.affine_weight_option initialise_option = ' -affine_init_matrix {init_transform_path}' if do_fod_registration: -<<<<<<< HEAD lmax_option = f' -affine_lmax {lmax}' if linear_estimator is not None: metric_option = f' -affine_metric.diff.estimator {linear_estimator}' -======= - lmax_option = ' -affine_lmax ' + str(lmax) - if linear_estimator is not None: - metric_option = ' -affine_metric.diff.estimator ' + linear_estimator ->>>>>>> origin/dev if write_log: mrregister_log_option = f' -info -affine_log {linear_log_path}' From 55929b06cd87550a940fa9328ebbe79ddb259e83 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 29 Feb 2024 11:05:58 +1100 Subject: [PATCH 44/75] 5ttgen: Algorithms return path of final image to test using 5ttcheck --- python/bin/5ttgen | 15 +++++++-------- python/lib/mrtrix3/_5ttgen/freesurfer.py | 2 ++ python/lib/mrtrix3/_5ttgen/fsl.py | 2 ++ python/lib/mrtrix3/_5ttgen/gif.py | 2 ++ python/lib/mrtrix3/_5ttgen/hsvs.py | 2 ++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/python/bin/5ttgen b/python/bin/5ttgen index 8e5d962899..c6e1bc6264 100755 --- a/python/bin/5ttgen +++ b/python/bin/5ttgen @@ -63,14 +63,13 @@ def execute(): #pylint: disable=unused-variable app.activate_scratch_dir() - # TODO Have algorithm return path to image to check - alg.execute() - - stderr = run.command('5ttcheck result.mif').stderr - if '[WARNING]' in stderr: - app.warn('Generated image does not perfectly conform to 5TT format:') - for line in stderr.splitlines(): - app.warn(line) + result_path = alg.execute() + if result_path: + stderr = run.command(['5ttcheck', result_path]).stderr + if '[WARNING]' in stderr: + app.warn('Generated image does not perfectly conform to 5TT format:') + for line in stderr.splitlines(): + app.warn(line) diff --git a/python/lib/mrtrix3/_5ttgen/freesurfer.py b/python/lib/mrtrix3/_5ttgen/freesurfer.py index 7b0b670ec6..f7fc7af05e 100644 --- a/python/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/python/lib/mrtrix3/_5ttgen/freesurfer.py @@ -90,3 +90,5 @@ def execute(): #pylint: disable=unused-variable mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) + + return 'result.mif' diff --git a/python/lib/mrtrix3/_5ttgen/fsl.py b/python/lib/mrtrix3/_5ttgen/fsl.py index 3786e6249b..22a499266a 100644 --- a/python/lib/mrtrix3/_5ttgen/fsl.py +++ b/python/lib/mrtrix3/_5ttgen/fsl.py @@ -275,3 +275,5 @@ def execute(): #pylint: disable=unused-variable mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) + + return 'result.mif' diff --git a/python/lib/mrtrix3/_5ttgen/gif.py b/python/lib/mrtrix3/_5ttgen/gif.py index e52352a971..3cbc13d925 100644 --- a/python/lib/mrtrix3/_5ttgen/gif.py +++ b/python/lib/mrtrix3/_5ttgen/gif.py @@ -70,3 +70,5 @@ def execute(): #pylint: disable=unused-variable mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) + + return 'result.mif' diff --git a/python/lib/mrtrix3/_5ttgen/hsvs.py b/python/lib/mrtrix3/_5ttgen/hsvs.py index c258c133f2..a3178c81c8 100644 --- a/python/lib/mrtrix3/_5ttgen/hsvs.py +++ b/python/lib/mrtrix3/_5ttgen/hsvs.py @@ -940,3 +940,5 @@ def voxel2scanner(voxel, header): run.command(['mrconvert', 'result.mif', app.ARGS.output], mrconvert_keyval=os.path.join(app.ARGS.input, 'mri', 'aparc+aseg.mgz'), force=app.FORCE_OVERWRITE) + + return 'result.mif' From db3ca002089c6077067386b05d891215d70b6d11 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 29 Feb 2024 11:06:15 +1100 Subject: [PATCH 45/75] Resolution of pylint errors for #2678 --- python/lib/mrtrix3/app.py | 3 +-- python/lib/mrtrix3/dwi2response/manual.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 7bfd6ec358..e1aa8b1273 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -566,7 +566,6 @@ def _get_message(self): class _FilesystemPath(pathlib.Path): def __new__(cls, *args, **kwargs): - # TODO Can we use positional arguments rather than kwargs? root_dir = kwargs.pop('root_dir', None) assert root_dir is not None return super().__new__(_WindowsPath if os.name == 'nt' else _PosixPath, @@ -618,7 +617,7 @@ def check_output(self): '(use -force to overwrite)') # Force parents=True for user-specified path # Force exist_ok=False for user-specified path - def mkdir(self, mode=0o777): + def mkdir(self, mode=0o777): # pylint: disable=arguments-differ while True: if FORCE_OVERWRITE: try: diff --git a/python/lib/mrtrix3/dwi2response/manual.py b/python/lib/mrtrix3/dwi2response/manual.py index 1a2167a159..157e3f4297 100644 --- a/python/lib/mrtrix3/dwi2response/manual.py +++ b/python/lib/mrtrix3/dwi2response/manual.py @@ -44,7 +44,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable - # TODO Can usage() wipe this from the CLI? if os.path.exists('mask.mif'): app.warn('-mask option is ignored by algorithm "manual"') os.remove('mask.mif') From ff983de53e3087afb7fdea229e60dab8a4c53aa5 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 1 Mar 2024 16:36:01 +1100 Subject: [PATCH 46/75] Python CLI: Fixes to typing & alternative typing interface - With previous code, filesystem types were not actually being constructed of the desirted type; instead, they were all being constructed as pathlib.PosixPath. This meant that additional checks intended to run after detection of presence of the -force option were not being performed. This was due to the slightly unusual construction of pathlib.Path making it difficult to produce further derived classes. Here, I instead make use of a convenience function to construct a new class that inherits both from the appropriate pathlib object for the host system, and from a class that intends to provide extensions atop that class. This should make checks for pre-existing output paths at commencement of execution work as intended, including for population_template where positional arguments have to be re-cast within the execute() function. - In population_template, option -transformed_dir now has its own explicit command-line interface type as a comma-separated list of output directories. - Add better checking for Python CLI behaviour for filesystem path types. --- .../commands/population_template.rst | 4 +- python/bin/population_template | 41 +++++---- python/lib/mrtrix3/app.py | 90 ++++++++----------- testing/bin/testing_python_cli | 7 +- testing/tests/python_cli | 29 +++--- 5 files changed, 84 insertions(+), 87 deletions(-) diff --git a/docs/reference/commands/population_template.rst b/docs/reference/commands/population_template.rst index 80bf22150a..ba9fbae166 100644 --- a/docs/reference/commands/population_template.rst +++ b/docs/reference/commands/population_template.rst @@ -38,7 +38,7 @@ Options Input, output and general options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- **-type choice** Specify the types of registration stages to perform. Options are: "rigid" (perform rigid registration only, which might be useful for intra-subject registration in longitudinal analysis); "affine" (perform affine registration); "nonlinear"; as well as cominations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear +- **-type choice** Specify the types of registration stages to perform. Options are: "rigid" (perform rigid registration only, which might be useful for intra-subject registration in longitudinal analysis); "affine" (perform affine registration); "nonlinear"; as well as combinations of registration types: "rigid_affine", "rigid_nonlinear", "affine_nonlinear", "rigid_affine_nonlinear". Default: rigid_affine_nonlinear - **-voxel_size values** Define the template voxel size in mm. Use either a single value for isotropic voxels or 3 comma-separated values. @@ -48,7 +48,7 @@ Input, output and general options - **-warp_dir directory** Output a directory containing warps from each input to the template. If the folder does not exist it will be created -- **-transformed_dir directory** Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, this path will contain a sub-directory for the images per contrast. +- **-transformed_dir directory_list** Output a directory containing the input images transformed to the template. If the folder does not exist it will be created. For multi-contrast registration, provide a comma-separated list of directories. - **-linear_transformations_dir directory** Output a directory containing the linear transformations used to generate the template. If the folder does not exist it will be created diff --git a/python/bin/population_template b/python/bin/population_template index 46064ed8c9..df0ab5c2ab 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -195,6 +195,16 @@ def usage(cmdline): #pylint: disable=unused-variable help='The gradient step size for non-linear registration' f' (Default: {DEFAULT_NL_GRAD_STEP})') + class SequenceDirectoryOut(app.Parser.CustomTypeBase): + def __call__(self, input_value): + return [app._make_userpath_object(app._UserDirOutPathExtras, item) for item in input_value.split(',')] + @staticmethod + def _legacytypestring(): + return 'SEQDIROUT' + @staticmethod + def _metavar(): + return 'directory_list' + options = cmdline.add_argument_group('Input, output and general options') registration_modes_string = ', '.join(f'"{x}"' for x in REGISTRATION_MODES if '_' in x) options.add_argument('-type', @@ -205,7 +215,7 @@ def usage(cmdline): #pylint: disable=unused-variable ' which might be useful for intra-subject registration in longitudinal analysis);' ' "affine" (perform affine registration);' ' "nonlinear";' - f' as well as cominations of registration types: {registration_modes_string}.' + f' as well as combinations of registration types: {registration_modes_string}.' ' Default: rigid_affine_nonlinear', default='rigid_affine_nonlinear') options.add_argument('-voxel_size', @@ -235,14 +245,12 @@ def usage(cmdline): #pylint: disable=unused-variable type=app.Parser.DirectoryOut(), help='Output a directory containing warps from each input to the template.' ' If the folder does not exist it will be created') - # TODO Would prefer for this to be exclusively a directory; - # but to do so will need to provide some form of disambiguation of multi-contrast files options.add_argument('-transformed_dir', - type=app.Parser.DirectoryOut(), + type=SequenceDirectoryOut(), help='Output a directory containing the input images transformed to the template.' ' If the folder does not exist it will be created.' ' For multi-contrast registration,' - ' this path will contain a sub-directory for the images per contrast.') + ' provide a comma-separated list of directories.') options.add_argument('-linear_transformations_dir', type=app.Parser.DirectoryOut(), help='Output a directory containing the linear transformations' @@ -809,27 +817,28 @@ def execute(): #pylint: disable=unused-variable assert (dorigid + doaffine + dononlinear >= 1), 'FIXME: registration type not valid' + # Reorder arguments for multi-contrast registration as after command line parsing app.ARGS.input_dir holds all but one argument + # Also cast user-speficied options to the correct types input_output = app.ARGS.input_dir + [app.ARGS.template] n_contrasts = len(input_output) // 2 if len(input_output) != 2 * n_contrasts: - raise MRtrixError(f'Expected two arguments per contrast, received {len(input_output)}: {", ".join(input_output)}') - if n_contrasts > 1: - app.console('Generating population template using multi-contrast registration') - - # reorder arguments for multi-contrast registration as after command line parsing app.ARGS.input_dir holds all but one argument - # TODO Write these to new variables rather than overwring app.ARGS? - # Or maybe better, invoke the appropriate typed arguments for each + raise MRtrixError('Expected two arguments per contrast;' + f' received {len(input_output)}:' + f' {", ".join(input_output)}') app.ARGS.input_dir = [] app.ARGS.template = [] for i_contrast in range(n_contrasts): inargs = (input_output[i_contrast*2], input_output[i_contrast*2+1]) - app.ARGS.input_dir.append(app.Parser.DirectoryIn(inargs[0])) - app.ARGS.template.append(app.Parser.ImageOut(inargs[1])) + app.ARGS.input_dir.append(app._make_userpath_object(app._UserPathExtras, inargs[0])) + app.ARGS.template.append(app._make_userpath_object(app._UserOutPathExtras, inargs[1])) # Perform checks that otherwise would have been done immediately after command-line parsing # were it not for the inability to represent input-output pairs in the command-line interface representation for output_path in app.ARGS.template: output_path.check_output() + if n_contrasts > 1: + app.console('Generating population template using multi-contrast registration') + cns = Contrasts() app.debug(str(cns)) @@ -931,9 +940,9 @@ def execute(): #pylint: disable=unused-variable app.ARGS.warp_dir = relpath(app.ARGS.warp_dir) if app.ARGS.transformed_dir: - app.ARGS.transformed_dir = [app.Parser.DirectoryOut(d) for d in app.ARGS.transformed_dir.split(',')] if len(app.ARGS.transformed_dir) != n_contrasts: - raise MRtrixError('require multiple comma separated transformed directories if multi-contrast registration is used') + raise MRtrixError(f'number of output directories specified for transformed images ({len(app.ARGS.transformed_dir)})' + f' does not match number of contrasts ({n_contrasts})') for tdir in app.ARGS.transformed_dir: tdir.check_output() diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index e1aa8b1273..6bc383df57 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -186,7 +186,7 @@ def _execute(module): #pylint: disable=unused-variable # Now that FORCE_OVERWRITE has been set, # check any user-specified output paths for arg in vars(ARGS): - if isinstance(arg, UserPath): + if isinstance(arg, _UserOutPathExtras): arg.check_output() # ANSI settings may have been altered at the command-line @@ -562,59 +562,47 @@ def _get_message(self): +# Function that will create a new class, +# which will derive from both pathlib.Path (which itself through __new__() could be Posix or Windows) +# and a desired augmentation that provides additional functions +def _make_userpath_object(base_class, *args, **kwargs): + normpath = os.path.normpath(os.path.join(WORKING_DIR, *args)) + new_class = type(f'{base_class.__name__.lstrip("_").rstrip("Extras")}', + (pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath, + base_class), + {}) + instance = new_class.__new__(new_class, normpath, **kwargs) + return instance -class _FilesystemPath(pathlib.Path): - def __new__(cls, *args, **kwargs): - root_dir = kwargs.pop('root_dir', None) - assert root_dir is not None - return super().__new__(_WindowsPath if os.name == 'nt' else _PosixPath, - os.path.normpath(os.path.join(root_dir, *args)), - **kwargs) + +class _UserPathExtras: def __format__(self, _): return shlex.quote(str(self)) -class _WindowsPath(pathlib.PureWindowsPath, _FilesystemPath): - _flavour = pathlib._windows_flavour # pylint: disable=protected-access -class _PosixPath(pathlib.PurePosixPath, _FilesystemPath): - _flavour = pathlib._posix_flavour # pylint: disable=protected-access - -class UserPath(_FilesystemPath): - def __new__(cls, *args, **kwargs): - kwargs.update({'root_dir': WORKING_DIR}) - return super().__new__(cls, *args, **kwargs) - def check_output(self): - pass - -class ScratchPath(_FilesystemPath): # pylint: disable=unused-variable - def __new__(cls, *args, **kwargs): - assert SCRATCH_DIR is not None - kwargs.update({'root_dir': SCRATCH_DIR}) - return super().__new__(cls, *args, **kwargs) - -class _UserFileOutPath(UserPath): +class _UserOutPathExtras(_UserPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) - def check_output(self): + def check_output(self, item_type='path'): if self.exists(): if FORCE_OVERWRITE: - warn(f'Output file path "{str(self)}" already exists; ' + warn(f'Output file {item_type} "{str(self)}" already exists; ' 'will be overwritten at script completion') else: - raise MRtrixError(f'Output file "{str(self)}" already exists ' + raise MRtrixError(f'Output {item_type} "{str(self)}" already exists ' '(use -force to override)') -class _UserDirOutPath(UserPath): +class _UserFileOutPathExtras(_UserOutPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) def check_output(self): - if self.exists(): - if FORCE_OVERWRITE: - warn(f'Output directory path "{str(self)}" already exists; ' - 'will be overwritten at script completion') - else: - raise MRtrixError(f'Output directory "{str(self)}" already exists ' - '(use -force to overwrite)') + return super().check_output('file') + +class _UserDirOutPathExtras(_UserOutPathExtras): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + def check_output(self): + return super().check_output('directory') # Force parents=True for user-specified path # Force exist_ok=False for user-specified path def mkdir(self, mode=0o777): # pylint: disable=arguments-differ @@ -642,8 +630,6 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ class Parser(argparse.ArgumentParser): - - # Various callable types for use as argparse argument types class CustomTypeBase: @staticmethod @@ -746,7 +732,7 @@ def _metavar(): class DirectoryIn(CustomTypeBase): def __call__(self, input_value): - abspath = UserPath(input_value) + abspath = _make_userpath_object(_UserPathExtras, input_value) if not abspath.exists(): raise argparse.ArgumentTypeError(f'Input directory "{input_value}" does not exist') if not abspath.is_dir(): @@ -759,10 +745,9 @@ def _legacytypestring(): def _metavar(): return 'directory' - class DirectoryOut(CustomTypeBase): def __call__(self, input_value): - abspath = _UserDirOutPath(input_value) + abspath = _make_userpath_object(_UserDirOutPathExtras, input_value) return abspath @staticmethod def _legacytypestring(): @@ -773,7 +758,7 @@ def _metavar(): class FileIn(CustomTypeBase): def __call__(self, input_value): - abspath = UserPath(input_value) + abspath = _make_userpath_object(_UserPathExtras, input_value) if not abspath.exists(): raise argparse.ArgumentTypeError(f'Input file "{input_value}" does not exist') if not abspath.is_file(): @@ -788,8 +773,7 @@ def _metavar(): class FileOut(CustomTypeBase): def __call__(self, input_value): - abspath = _UserFileOutPath(input_value) - return abspath + return _make_userpath_object(_UserFileOutPathExtras, input_value) @staticmethod def _legacytypestring(): return 'FILEOUT' @@ -801,9 +785,10 @@ class ImageIn(CustomTypeBase): def __call__(self, input_value): if input_value == '-': input_value = sys.stdin.readline().strip() - _STDIN_IMAGES.append(input_value) - abspath = UserPath(input_value) - return abspath + abspath = pathlib.Path(input_value) + _STDIN_IMAGES.append(abspath) + return abspath + return _make_userpath_object(_UserPathExtras, input_value) @staticmethod def _legacytypestring(): return 'IMAGEIN' @@ -815,11 +800,12 @@ class ImageOut(CustomTypeBase): def __call__(self, input_value): if input_value == '-': input_value = utils.name_temporary('mif') - _STDOUT_IMAGES.append(input_value) + abspath = pathlib.Path(input_value) + _STDOUT_IMAGES.append(abspath) + return abspath # Not guaranteed to catch all cases of output images trying to overwrite existing files; # but will at least catch some of them - abspath = _UserFileOutPath(input_value) - return abspath + return _make_userpath_object(_UserFileOutPathExtras, input_value) @staticmethod def _legacytypestring(): return 'IMAGEOUT' diff --git a/testing/bin/testing_python_cli b/testing/bin/testing_python_cli index 29390ddaf8..38888be58e 100755 --- a/testing/bin/testing_python_cli +++ b/testing/bin/testing_python_cli @@ -122,14 +122,17 @@ def usage(cmdline): #pylint: disable=unused-variable custom.add_argument('-tracks_out', type=app.Parser.TracksOut(), help='An output tractogram') + custom.add_argument('-various', + type=app.Parser.Various(), + help='An option that accepts various types of content') def execute(): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - for key, value in app.ARGS.iteritems(): - app.console(f'{key}: {value}') + for key in vars(app.ARGS): + app.console(f'{key}: {repr(getattr(app.ARGS, key))}') diff --git a/testing/tests/python_cli b/testing/tests/python_cli index d2152510d2..8561f95fbf 100644 --- a/testing/tests/python_cli +++ b/testing/tests/python_cli @@ -1,4 +1,4 @@ -mkdir -p tmp-newdirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -intseq 1,2,3 -floatseq 0.1,0.2,0.3 -dirin tmp-dirin/ -dirout tmp-dirout/ -filein tmp-filein.txt -fileout tmp-fileout.txt -tracksin tmp-tracksin.tck -tracksout tmp-tracksout.tck -various my_various +mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various testing_python_cli -help > tmp.txt && diff -q tmp.txt data/python_cli/help.txt testing_python_cli __print_full_usage__ > tmp.txt && diff -q tmp.txt data/python_cli/full_usage.txt testing_python_cli __print_usage_markdown__ > tmp.md && diff -q tmp.md data/python_cli/markdown.md @@ -23,17 +23,16 @@ testing_python_cli -float_builtin NotAFloat && false || true testing_python_cli -float_unbound NotAFloat && false || true testing_python_cli -float_nonneg -0.1 && false || true testing_python_cli -float_bound 1.1 && false || true -testing_python_cli -intseq 0.1,0.2,0.3 && false || true -testing_python_cli -intseq Not,An,Int,Seq && false || true -testing_python_cli -floatseq Not,A,Float,Seq && false || true -testing_python_cli -dirin does/not/exist/ && false || true -mkdir -p tmp-dirout/ && testing_python_cli -dirout tmp-dirout/ && false || true -testing_python_cli -dirout tmp-dirout/ -force -testing_python_cli -filein does/not/exist.txt && false || true -touch tmp-fileout.txt && testing_python_cli -fileout tmp-fileout.txt && false || true -touch tmp-fileout.txt && testing_python_cli -fileout tmp-fileout.txt -force -testing_python_cli -tracksin does/not/exist.txt && false || true -testing_python_cli -tracksin tmp-filein.txt && false || true -touch tmp-tracksout.tck && testing_python_cli -tracksout tmp-tracksout.tck && false || true -testing_python_cli -tracksout tmp-tracksout.tck -force -testing_python_cli -tracksout tmp-tracksout.txt && false || true +testing_python_cli -int_seq 0.1,0.2,0.3 && false || true +testing_python_cli -int_seq Not,An,Int,Seq && false || true +testing_python_cli -float_seq Not,A,Float,Seq && false || true +rm -rf tmp-dirin/ && testing_python_cli -dir_in tmp-dirin/ && false || true +mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ 2>&1 && grep "use -force to override" +mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ -force +rm -f tmp-filein.txt && testing_python_cli -file_in tmp-filein.txt && false || true +touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt 2>&1 && grep "use -force to override" +touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt -force +rm -f tmp-tracksin.tck && testing_python_cli -tracks_in tmp-tracksin.tck && false || true +touch tmp-filein.txt && testing_python_cli -tracks_in tmp-filein.txt && false || true +touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck 2>&1 && grep "use -force to override" +touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck -force From 553a4fb48647fed0a949f87cd81e8f7fa013ec75 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 1 Mar 2024 19:05:12 +1100 Subject: [PATCH 47/75] Python CLI: Move relevant functions / classes into Parser class --- python/bin/population_template | 3 +- python/lib/mrtrix3/app.py | 128 +++++++++++++++++---------------- 2 files changed, 67 insertions(+), 64 deletions(-) diff --git a/python/bin/population_template b/python/bin/population_template index df0ab5c2ab..214f781a1b 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -197,7 +197,8 @@ def usage(cmdline): #pylint: disable=unused-variable class SequenceDirectoryOut(app.Parser.CustomTypeBase): def __call__(self, input_value): - return [app._make_userpath_object(app._UserDirOutPathExtras, item) for item in input_value.split(',')] + return [cmdline.make_userpath_object(app._UserDirOutPathExtras, item) # pylint: disable=protected-access \ + for item in input_value.split(',')] @staticmethod def _legacytypestring(): return 'SEQDIROUT' diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 6bc383df57..5239651c66 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -562,63 +562,11 @@ def _get_message(self): -# Function that will create a new class, -# which will derive from both pathlib.Path (which itself through __new__() could be Posix or Windows) -# and a desired augmentation that provides additional functions -def _make_userpath_object(base_class, *args, **kwargs): - normpath = os.path.normpath(os.path.join(WORKING_DIR, *args)) - new_class = type(f'{base_class.__name__.lstrip("_").rstrip("Extras")}', - (pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath, - base_class), - {}) - instance = new_class.__new__(new_class, normpath, **kwargs) - return instance - - - -class _UserPathExtras: - def __format__(self, _): - return shlex.quote(str(self)) - -class _UserOutPathExtras(_UserPathExtras): - def __init__(self, *args, **kwargs): - super().__init__(self, *args, **kwargs) - def check_output(self, item_type='path'): - if self.exists(): - if FORCE_OVERWRITE: - warn(f'Output file {item_type} "{str(self)}" already exists; ' - 'will be overwritten at script completion') - else: - raise MRtrixError(f'Output {item_type} "{str(self)}" already exists ' - '(use -force to override)') - -class _UserFileOutPathExtras(_UserOutPathExtras): - def __init__(self, *args, **kwargs): - super().__init__(self, *args, **kwargs) - def check_output(self): - return super().check_output('file') - -class _UserDirOutPathExtras(_UserOutPathExtras): - def __init__(self, *args, **kwargs): - super().__init__(self, *args, **kwargs) - def check_output(self): - return super().check_output('directory') - # Force parents=True for user-specified path - # Force exist_ok=False for user-specified path - def mkdir(self, mode=0o777): # pylint: disable=arguments-differ - while True: - if FORCE_OVERWRITE: - try: - shutil.rmtree(self) - except OSError: - pass - try: - super().mkdir(mode, parents=True, exist_ok=False) - return - except FileExistsError: - if not FORCE_OVERWRITE: - raise MRtrixError(f'Output directory "{str(self)}" already exists ' # pylint: disable=raise-missing-from - '(use -force to override)') + + + + + @@ -630,6 +578,60 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ class Parser(argparse.ArgumentParser): + # Function that will create a new class, + # which will derive from both pathlib.Path (which itself through __new__() could be Posix or Windows) + # and a desired augmentation that provides additional functions + def make_userpath_object(base_class, *args, **kwargs): + normpath = os.path.normpath(os.path.join(WORKING_DIR, *args)) + new_class = type(f'{base_class.__name__.lstrip("_").rstrip("Extras")}', + (pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath, + base_class), + {}) + instance = new_class.__new__(new_class, normpath, **kwargs) + return instance + + # Classes that extend the functionality of pathlib.Path + class _UserPathExtras: + def __format__(self, _): + return shlex.quote(str(self)) + class _UserOutPathExtras(_UserPathExtras): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + def check_output(self, item_type='path'): + if self.exists(): + if FORCE_OVERWRITE: + warn(f'Output file {item_type} "{str(self)}" already exists; ' + 'will be overwritten at script completion') + else: + raise MRtrixError(f'Output {item_type} "{str(self)}" already exists ' + '(use -force to override)') + class _UserFileOutPathExtras(_UserOutPathExtras): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + def check_output(self): + return super().check_output('file') + class _UserDirOutPathExtras(_UserOutPathExtras): + def __init__(self, *args, **kwargs): + super().__init__(self, *args, **kwargs) + def check_output(self): + return super().check_output('directory') + # Force parents=True for user-specified path + # Force exist_ok=False for user-specified path + def mkdir(self, mode=0o777): # pylint: disable=arguments-differ + while True: + if FORCE_OVERWRITE: + try: + shutil.rmtree(self) + except OSError: + pass + try: + super().mkdir(mode, parents=True, exist_ok=False) + return + except FileExistsError: + if not FORCE_OVERWRITE: + raise MRtrixError(f'Output directory "{str(self)}" already exists ' # pylint: disable=raise-missing-from + '(use -force to override)') + # Various callable types for use as argparse argument types class CustomTypeBase: @staticmethod @@ -732,7 +734,7 @@ def _metavar(): class DirectoryIn(CustomTypeBase): def __call__(self, input_value): - abspath = _make_userpath_object(_UserPathExtras, input_value) + abspath = Parser.make_userpath_object(Parser._UserPathExtras, input_value) if not abspath.exists(): raise argparse.ArgumentTypeError(f'Input directory "{input_value}" does not exist') if not abspath.is_dir(): @@ -747,7 +749,7 @@ def _metavar(): class DirectoryOut(CustomTypeBase): def __call__(self, input_value): - abspath = _make_userpath_object(_UserDirOutPathExtras, input_value) + abspath = Parser.make_userpath_object(Parser._UserDirOutPathExtras, input_value) return abspath @staticmethod def _legacytypestring(): @@ -758,7 +760,7 @@ def _metavar(): class FileIn(CustomTypeBase): def __call__(self, input_value): - abspath = _make_userpath_object(_UserPathExtras, input_value) + abspath = Parser.make_userpath_object(Parser._UserPathExtras, input_value) if not abspath.exists(): raise argparse.ArgumentTypeError(f'Input file "{input_value}" does not exist') if not abspath.is_file(): @@ -773,7 +775,7 @@ def _metavar(): class FileOut(CustomTypeBase): def __call__(self, input_value): - return _make_userpath_object(_UserFileOutPathExtras, input_value) + return Parser.make_userpath_object(Parser._UserFileOutPathExtras, input_value) @staticmethod def _legacytypestring(): return 'FILEOUT' @@ -788,7 +790,7 @@ def __call__(self, input_value): abspath = pathlib.Path(input_value) _STDIN_IMAGES.append(abspath) return abspath - return _make_userpath_object(_UserPathExtras, input_value) + return Parser.make_userpath_object(Parser._UserPathExtras, input_value) @staticmethod def _legacytypestring(): return 'IMAGEIN' @@ -805,7 +807,7 @@ def __call__(self, input_value): return abspath # Not guaranteed to catch all cases of output images trying to overwrite existing files; # but will at least catch some of them - return _make_userpath_object(_UserFileOutPathExtras, input_value) + return Parser.make_userpath_object(Parser._UserFileOutPathExtras, input_value) @staticmethod def _legacytypestring(): return 'IMAGEOUT' From e25e977992fe35eb78b09e5e0d6f3b40cfc3a472 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 1 Mar 2024 21:07:34 +1100 Subject: [PATCH 48/75] Fix for population_template for #2678 - Remove remaining TODO directives; some of these changes will be deferred for a later overhaul of population_template. - Fixes to reflect parent commits; eg. movement of functions and classes relating to CLI from mrtrix3.app to mrtrix3.app.Parser. - Multiple population_template bug fixes: - Erroneous path to afine matrix decomposition. - Preserve the native type of app.ARGS.mask, since the CLI is now responsible for yielding an absolute path that can be used irrespective of working directory. - Fix number of f-string transition failures. - Do not attempt to read from initial linear transformation in first iteration if initial alignment is explicitly disabled. - Do not attempt to perform linear drift correction if no initial alignment was performed. - Remove use of path.make_dir() in population_template. - Multiple minor bug fixes around filesystem path CLI types. --- python/bin/population_template | 74 ++++++++++------------- python/lib/mrtrix3/app.py | 38 ++++++------ testing/scripts/tests/population_template | 2 +- 3 files changed, 52 insertions(+), 62 deletions(-) diff --git a/python/bin/population_template b/python/bin/population_template index 214f781a1b..61efaa864a 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -16,9 +16,6 @@ # For more details, see http://www.mrtrix.org/. # Generates an unbiased group-average template via image registration of images to a midway space. -# TODO Make use of pathlib throughout; should be able to remove shlex dependency -# TODO Consider asserting that anything involving image paths in this script -# be based on pathlib rather than strings import json, math, os, re, shlex, shutil, sys DEFAULT_RIGID_SCALES = [0.3,0.4,0.6,0.8,1.0,1.0] @@ -197,7 +194,7 @@ def usage(cmdline): #pylint: disable=unused-variable class SequenceDirectoryOut(app.Parser.CustomTypeBase): def __call__(self, input_value): - return [cmdline.make_userpath_object(app._UserDirOutPathExtras, item) # pylint: disable=protected-access \ + return [cmdline.make_userpath_object(app.Parser._UserDirOutPathExtras, item) # pylint: disable=protected-access \ for item in input_value.split(',')] @staticmethod def _legacytypestring(): @@ -337,7 +334,7 @@ def check_linear_transformation(transformation, cmd, max_scaling=0.5, max_shear= app.console(f'"{transformation}decomp" not found; skipping check') return True data = utils.load_keyval(f'{transformation}decomp') - run.function(os.remove, f'{transformation}_decomp') + run.function(os.remove, f'{transformation}decomp') scaling = [float(value) for value in data['scaling']] if any(a < 0 for a in scaling) or any(a > (1 + max_scaling) for a in scaling) or any( a < (1 - max_scaling) for a in scaling): @@ -455,7 +452,6 @@ def get_common_prefix(file_list): return os.path.commonprefix(file_list) -# Todo Create singular "Contrast" class class Contrasts: """ Class that parses arguments and holds information specific to each image contrast @@ -538,8 +534,6 @@ class Contrasts: def n_contrasts(self): return len(self.suff) - # TODO Obey expected formatting of __repr__() - # (or just remove) def __repr__(self, *args, **kwargs): text = '' for cid in range(self.n_contrasts): @@ -637,16 +631,14 @@ class Input: return ', '.join(message) def cache_local(self): - from mrtrix3 import run, path # pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import run # pylint: disable=no-name-in-module, import-outside-toplevel contrasts = self.contrasts for cid, csuff in enumerate(contrasts): - if not os.path.isdir(f'input{csuff}'): - path.make_dir(f'input{csuff}') + os.makedirs(f'input{csuff}', exist_ok=True) run.command(['mrconvert', self.ims_path[cid], os.path.join(f'input{csuff}', f'{self.uid}.mif')]) self._local_ims = [os.path.join(f'input{csuff}', f'{self.uid}.mif') for csuff in contrasts] if self.msk_filename: - if not os.path.isdir('mask'): - path.make_dir('mask') + os.makedirs('mask', exist_ok=True) run.command(['mrconvert', self.msk_path, os.path.join('mask', f'{self.uid}.mif')]) self._local_msk = os.path.join('mask', f'{self.uid}.mif') @@ -801,6 +793,7 @@ def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whites return inputs, xcontrast_xsubject_pre_postfix + def execute(): #pylint: disable=unused-variable from mrtrix3 import MRtrixError, app, image, matrix, path, run, EXE_LIST #pylint: disable=no-name-in-module, import-outside-toplevel @@ -830,8 +823,8 @@ def execute(): #pylint: disable=unused-variable app.ARGS.template = [] for i_contrast in range(n_contrasts): inargs = (input_output[i_contrast*2], input_output[i_contrast*2+1]) - app.ARGS.input_dir.append(app._make_userpath_object(app._UserPathExtras, inargs[0])) - app.ARGS.template.append(app._make_userpath_object(app._UserOutPathExtras, inargs[1])) + app.ARGS.input_dir.append(app.Parser.make_userpath_object(app.Parser._UserPathExtras, inargs[0])) # pylint: disable=protected-access + app.ARGS.template.append(app.Parser.make_userpath_object(app.Parser._UserOutPathExtras, inargs[1])) # pylint: disable=protected-access # Perform checks that otherwise would have been done immediately after command-line parsing # were it not for the inability to represent input-output pairs in the command-line interface representation for output_path in app.ARGS.template: @@ -890,12 +883,10 @@ def execute(): #pylint: disable=unused-variable mask_files = [] if app.ARGS.mask_dir: use_masks = True - app.ARGS.mask_dir = relpath(app.ARGS.mask_dir) - if not os.path.isdir(app.ARGS.mask_dir): - raise MRtrixError('Mask directory not found') mask_files = sorted(path.all_in_dir(app.ARGS.mask_dir, dir_path=False)) if len(mask_files) < len(in_files[0]): - raise MRtrixError('There are not enough mask images for the number of images in the input directory') + raise MRtrixError(f'There are not enough mask images ({len(mask_files)})' + f' for the number of images in the input directory ({len(in_files[0])})') if not use_masks: app.warn('No masks input; use of input masks is recommended to reduce computation time and improve robustness') @@ -970,7 +961,7 @@ def execute(): #pylint: disable=unused-variable cns.n_volumes.append(0) if do_fod_registration: fod_contrasts_dirs = [app.ARGS.input_dir[cid] for cid in range(n_contrasts) if cns.fod_reorientation[cid]] - app.console(f'SH Series detected, performing FOD registration in contrast: {", ".join(fod_contrasts_dirs)}') + app.console(f'SH Series detected, performing FOD registration in contrast: {", ".join(map(str, fod_contrasts_dirs))}') c_mrtransform_reorientation = [' -reorient_fod ' + ('yes' if cns.fod_reorientation[cid] else 'no') + ' ' for cid in range(n_contrasts)] @@ -1123,7 +1114,7 @@ def execute(): #pylint: disable=unused-variable app.console(f'({istage:02d}) nonlinear scale: {scale:.4f}, niter: {niter}, lmax: {lmax}') else: for istage, [scale, niter] in enumerate(zip(nl_scales, nl_niter)): - app.console('({istage:02d}) nonlinear scale: {scale:.4f}, niter: {niter}, no reorientation') + app.console(f'({istage:02d}) nonlinear scale: {scale:.4f}, niter: {niter}, no reorientation') app.console('-' * 60) app.console('input images:') @@ -1134,28 +1125,28 @@ def execute(): #pylint: disable=unused-variable app.activate_scratch_dir() for contrast in cns.suff: - path.make_dir(f'input_transformed{contrast}') + os.mkdir(f'input_transformed{contrast}') for contrast in cns.suff: - path.make_dir(f'isfinite{contrast}') + os.mkdir(f'isfinite{contrast}') - path.make_dir('linear_transforms_initial') - path.make_dir('linear_transforms') + os.mkdir('linear_transforms_initial') + os.mkdir('linear_transforms') for level in range(0, len(linear_scales)): - path.make_dir(f'linear_transforms_{level:02d}') + os.mkdir(f'linear_transforms_{level:02d}') for level in range(0, len(nl_scales)): - path.make_dir(f'warps_{level:02d}') + os.mkdir(f'warps_{level:02d}') if use_masks: - path.make_dir('mask_transformed') + os.mkdir('mask_transformed') write_log = (app.VERBOSITY >= 2) if write_log: - path.make_dir('log') + os.mkdir('log') if initial_alignment == 'robust_mass': if not use_masks: raise MRtrixError('robust_mass initial alignment requires masks') - path.make_dir('robust') + os.mkdir('robust') if app.ARGS.copy_input: app.console('Copying images into scratch directory') @@ -1388,8 +1379,11 @@ def execute(): #pylint: disable=unused-variable metric_option = '' mrregister_log_option = '' output_transform_path = os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') - init_transform_path = os.path.join(f'linear_transforms_{level-1:02d}' if level > 0 else 'linear_transforms_initial', - f'{inp.uid}.txt') + if initial_alignment == 'none': + init_transform_path = None + else: + init_transform_path = os.path.join(f'linear_transforms_{level-1:02d}' if level > 0 else 'linear_transforms_initial', + f'{inp.uid}.txt') linear_log_path = os.path.join('log', f'{inp.uid}{contrast[cid]}_{level}.log') if regtype == 'rigid': scale_option = f' -rigid_scale {scale}' @@ -1398,7 +1392,7 @@ def execute(): #pylint: disable=unused-variable output_option = f' -rigid {output_transform_path}' contrast_weight_option = cns.rigid_weight_option - initialise_option = f' -rigid_init_matrix {init_transform_path}' + initialise_option = f' -rigid_init_matrix {init_transform_path}' if init_transform_path else '' if do_fod_registration: lmax_option = f' -rigid_lmax {lmax}' if linear_estimator is not None: @@ -1409,9 +1403,9 @@ def execute(): #pylint: disable=unused-variable scale_option = f' -affine_scale {scale}' niter_option = f' -affine_niter {niter}' regtype_option = ' -type affine' - output_option = ' -affine {output_transform_path}' + output_option = f' -affine {output_transform_path}' contrast_weight_option = cns.affine_weight_option - initialise_option = ' -affine_init_matrix {init_transform_path}' + initialise_option = f' -affine_init_matrix {init_transform_path}' if init_transform_path else '' if do_fod_registration: lmax_option = f' -affine_lmax {lmax}' if linear_estimator is not None: @@ -1467,16 +1461,12 @@ def execute(): #pylint: disable=unused-variable # - Not sure whether it's preferable to stabilise E[ T_i^{-1} ] # - If one subject's registration fails, this will affect the average and therefore the template which could result in instable behaviour. # - The template appearance changes slightly over levels, but the template and trafos are affected in the same way so should not affect template convergence. - if not app.ARGS.linear_no_drift_correction: - # TODO Is this a bug? - # Seems to be averaging N instances of the last input, rather than averaging all inputs - # TODO Further, believe that the first transformcalc call only needs to be performed once; - # should not need to be recomputed between iterations + if not app.ARGS.linear_no_drift_correction and app.ARGS.initial_alignment != 'none': run.command(['transformcalc', [os.path.join('linear_transforms_initial', f'{inp.uid}.txt') for _inp in ins], 'average', 'linear_transform_average_init.txt', '-quiet'], force=True) run.command(['transformcalc', [os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') for _inp in ins], 'average', f'linear_transform_average_{level:02d}_uncorrected.txt', '-quiet'], force=True) - run.command(['transformcalc', 'linear_transform_average_{level:02d}_uncorrected.txt', + run.command(['transformcalc', f'linear_transform_average_{level:02d}_uncorrected.txt', 'invert', f'linear_transform_average_{level:02d}_uncorrected_inv.txt', '-quiet'], force=True) transform_average_init = matrix.load_transform('linear_transform_average_init.txt') @@ -1561,7 +1551,7 @@ def execute(): #pylint: disable=unused-variable force=True) if dononlinear: - path.make_dir('warps') + os.mkdir('warps') level = 0 def nonlinear_msg(): return f'Optimising template with non-linear registration (stage {level+1} of {len(nl_scales)})' diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 5239651c66..c0c07d731d 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -186,7 +186,7 @@ def _execute(module): #pylint: disable=unused-variable # Now that FORCE_OVERWRITE has been set, # check any user-specified output paths for arg in vars(ARGS): - if isinstance(arg, _UserOutPathExtras): + if isinstance(arg, Parser._UserOutPathExtras): # pylint: disable=protected-access arg.check_output() # ANSI settings may have been altered at the command-line @@ -296,9 +296,9 @@ def _execute(module): #pylint: disable=unused-variable debug(f'Erasing {len(_STDIN_IMAGES)} piped input images') for item in _STDIN_IMAGES: try: - os.remove(item) + item.unlink() debug(f'Successfully erased "{item}"') - except OSError as exc: + except FileNotFoundError as exc: debug(f'Unable to erase "{item}": {exc}') if SCRATCH_DIR: if DO_CLEANUP: @@ -313,7 +313,7 @@ def _execute(module): #pylint: disable=unused-variable console(f'Scratch directory retained; location: {SCRATCH_DIR}') if _STDOUT_IMAGES: debug(f'Emitting {len(_STDOUT_IMAGES)} output piped images to stdout') - sys.stdout.write('\n'.join(_STDOUT_IMAGES)) + sys.stdout.write('\n'.join(map(str, _STDOUT_IMAGES))) sys.exit(return_code) @@ -337,16 +337,15 @@ def activate_scratch_dir(): #pylint: disable=unused-variable random_string = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(6)) SCRATCH_DIR = os.path.join(dir_path, f'{prefix}{random_string}') + os.sep os.makedirs(SCRATCH_DIR) - console(f'Generated scratch directory: {SCRATCH_DIR}') - with open(os.path.join(SCRATCH_DIR, 'cwd.txt'), 'w', encoding='utf-8') as outfile: + os.chdir(SCRATCH_DIR) + if VERBOSITY: + console(f'Activated scratch directory: {SCRATCH_DIR}') + with open('cwd.txt', 'w', encoding='utf-8') as outfile: outfile.write(f'{WORKING_DIR}\n') - with open(os.path.join(SCRATCH_DIR, 'command.txt'), 'w', encoding='utf-8') as outfile: + with open('command.txt', 'w', encoding='utf-8') as outfile: outfile.write(f'{" ".join(sys.argv)}\n') - with open(os.path.join(SCRATCH_DIR, 'log.txt'), 'w', encoding='utf-8'): + with open('log.txt', 'w', encoding='utf-8'): pass - if VERBOSITY: - console(f'Changing to scratch directory ({SCRATCH_DIR})') - os.chdir(SCRATCH_DIR) # Also use this scratch directory for any piped images within run.command() calls, # and for keeping a log of executed commands / functions run.shared.set_scratch_dir(SCRATCH_DIR) @@ -581,13 +580,14 @@ class Parser(argparse.ArgumentParser): # Function that will create a new class, # which will derive from both pathlib.Path (which itself through __new__() could be Posix or Windows) # and a desired augmentation that provides additional functions + @staticmethod def make_userpath_object(base_class, *args, **kwargs): - normpath = os.path.normpath(os.path.join(WORKING_DIR, *args)) + abspath = os.path.normpath(os.path.join(WORKING_DIR, *args)) new_class = type(f'{base_class.__name__.lstrip("_").rstrip("Extras")}', - (pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath, - base_class), + (base_class, + pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath), {}) - instance = new_class.__new__(new_class, normpath, **kwargs) + instance = new_class.__new__(new_class, abspath, **kwargs) return instance # Classes that extend the functionality of pathlib.Path @@ -600,7 +600,7 @@ def __init__(self, *args, **kwargs): def check_output(self, item_type='path'): if self.exists(): if FORCE_OVERWRITE: - warn(f'Output file {item_type} "{str(self)}" already exists; ' + warn(f'Output {item_type} "{str(self)}" already exists; ' 'will be overwritten at script completion') else: raise MRtrixError(f'Output {item_type} "{str(self)}" already exists ' @@ -622,7 +622,7 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ if FORCE_OVERWRITE: try: shutil.rmtree(self) - except OSError: + except FileNotFoundError: pass try: super().mkdir(mode, parents=True, exist_ok=False) @@ -1563,7 +1563,7 @@ def handler(signum, _frame): sys.stderr.write(f'{EXEC_NAME}: {ANSI.console}Scratch directory retained; location: {SCRATCH_DIR}{ANSI.clear}\n') for item in _STDIN_IMAGES: try: - os.remove(item) - except OSError: + item.unlink() + except FileNotFoundError: pass os._exit(signum) # pylint: disable=protected-access diff --git a/testing/scripts/tests/population_template b/testing/scripts/tests/population_template index d1b0ed1618..34201fa617 100644 --- a/testing/scripts/tests/population_template +++ b/testing/scripts/tests/population_template @@ -1,7 +1,7 @@ mkdir -p ../tmp/population_template && mkdir -p tmp-mask && mkdir -p tmp-fa && mkdir -p tmp-fod && mrconvert BIDS/sub-02/dwi/sub-02_brainmask.nii.gz tmp-mask/sub-02.mif -force && mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz tmp-mask/sub-03.mif -force && dwi2tensor BIDS/sub-02/dwi/sub-02_dwi.nii.gz -fslgrad BIDS/sub-02/dwi/sub-02_dwi.bvec BIDS/sub-02/dwi/sub-02_dwi.bval -mask BIDS/sub-02/dwi/sub-02_brainmask.nii.gz - | tensor2metric - -fa tmp-fa/sub-02.mif -force && dwi2tensor BIDS/sub-03/dwi/sub-03_dwi.nii.gz -fslgrad BIDS/sub-03/dwi/sub-03_dwi.bvec BIDS/sub-03/dwi/sub-03_dwi.bval -mask BIDS/sub-03/dwi/sub-03_brainmask.nii.gz - | tensor2metric - -fa tmp-fa/sub-03.mif -force && population_template tmp-fa ../tmp/population_template/fa_default_template.mif -warp_dir ../tmp/population_template/fa_default_warpdir/ -transformed_dir ../tmp/population_template/fa_default_transformeddir/ -linear_transformations_dir ../tmp/population_template/fa_default_lineartransformsdir/ -force && testing_diff_image ../tmp/population_template/fa_default_template.mif population_template/fa_default_template.mif.gz -abs 0.01 population_template tmp-fa/ - | testing_diff_image - population_template/fa_default_template.mif.gz -abs 0.01 population_template tmp-fa/ ../tmp/population_template/fa_masked_template.mif -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_masked_mask.mif -force && testing_diff_image ../tmp/population_template/fa_masked_template.mif population_template/fa_masked_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_masked_mask.mif smooth -) $(mrfilter population_template/fa_masked_mask.mif.gz smooth -) -abs 0.3 -population_template tmp-fa/ ../tmp/population_template/fa_masked_template.mif -mask_dir tmp-mask/ -template_mask - | testing_diff_image $(mrfilter - smooth -) $(mrfilter population_template/fa_masked_mask.mif.gz smooth -) -abs 0.3 +population_template tmp-fa/ ../tmp/population_template/fa_masked_template.mif -mask_dir tmp-mask/ -template_mask - -force | testing_diff_image $(mrfilter - smooth -) $(mrfilter population_template/fa_masked_mask.mif.gz smooth -) -abs 0.3 population_template tmp-fa/ ../tmp/population_template/fa_rigid_template.mif -type rigid -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_rigid_mask.mif -force && testing_diff_image ../tmp/population_template/fa_rigid_template.mif population_template/fa_rigid_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_rigid_mask.mif smooth -) $(mrfilter population_template/fa_rigid_mask.mif.gz smooth -) -abs 0.3 population_template tmp-fa/ ../tmp/population_template/fa_affine_template.mif -type affine -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_affine_mask.mif -force && testing_diff_image ../tmp/population_template/fa_affine_template.mif population_template/fa_affine_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_affine_mask.mif smooth -) $(mrfilter population_template/fa_affine_mask.mif.gz smooth -) -abs 0.3 population_template tmp-fa/ ../tmp/population_template/fa_nonlinear_template.mif -type nonlinear -mask_dir tmp-mask/ -template_mask ../tmp/population_template/fa_nonlinear_mask.mif -force && testing_diff_image ../tmp/population_template/fa_nonlinear_template.mif population_template/fa_nonlinear_template.mif.gz -abs 0.01 && testing_diff_image $(mrfilter ../tmp/population_template/fa_nonlinear_mask.mif smooth -) $(mrfilter population_template/fa_nonlinear_mask.mif.gz smooth -) -abs 0.3 From 23f436d8dc438fd8ffdbd9d8d243abc761c4a131 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 4 Mar 2024 13:10:52 +1100 Subject: [PATCH 49/75] Python API: Fixes to typed CLI - Fix pylint warnings issued due to runtime inheritance. - Fix a few pieces of code that were not properly updated in ff983de53e3087afb7fdea229e60dab8a4c53aa5. --- python/lib/mrtrix3/_5ttgen/fsl.py | 15 --------------- python/lib/mrtrix3/app.py | 8 ++++---- python/lib/mrtrix3/dwi2mask/b02template.py | 2 +- python/lib/mrtrix3/fsl.py | 2 +- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/python/lib/mrtrix3/_5ttgen/fsl.py b/python/lib/mrtrix3/_5ttgen/fsl.py index 22a499266a..4881143f15 100644 --- a/python/lib/mrtrix3/_5ttgen/fsl.py +++ b/python/lib/mrtrix3/_5ttgen/fsl.py @@ -61,21 +61,6 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable -def get_inputs(): #pylint: disable=unused-variable - image.check_3d_nonunity(app.ARGS.input) - run.command(['mrconvert', app.ARGS.input, app.ScratchPath('input.mif')], - preserve_pipes=True) - if app.ARGS.mask: - run.command(['mrconvert', app.ARGS.mask, app.ScratchPath('mask.mif'), '-datatype', 'bit', '-strides', '-1,+2,+3'], - preserve_pipes=True) - if app.ARGS.t2: - if not image.match(app.ARGS.input, app.ARGS.t2): - raise MRtrixError('Provided T2w image does not match input T1w image') - run.command(['mrconvert', app.ARGS.t2, app.ScratchPath('T2.nii'), '-strides', '-1,+2,+3'], - preserve_pipes=True) - - - def execute(): #pylint: disable=unused-variable if utils.is_windows(): raise MRtrixError('"fsl" algorithm of 5ttgen script cannot be run on Windows: ' diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index c0c07d731d..fc14d0b632 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -598,7 +598,7 @@ class _UserOutPathExtras(_UserPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) def check_output(self, item_type='path'): - if self.exists(): + if self.exists(): # pylint: disable=no-member if FORCE_OVERWRITE: warn(f'Output {item_type} "{str(self)}" already exists; ' 'will be overwritten at script completion') @@ -608,12 +608,12 @@ def check_output(self, item_type='path'): class _UserFileOutPathExtras(_UserOutPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) - def check_output(self): + def check_output(self): # pylint: disable=arguments-differ return super().check_output('file') class _UserDirOutPathExtras(_UserOutPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) - def check_output(self): + def check_output(self): # pylint: disable=arguments-differ return super().check_output('directory') # Force parents=True for user-specified path # Force exist_ok=False for user-specified path @@ -625,7 +625,7 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ except FileNotFoundError: pass try: - super().mkdir(mode, parents=True, exist_ok=False) + super().mkdir(mode, parents=True, exist_ok=False) # pylint: disable=no-member return except FileExistsError: if not FORCE_OVERWRITE: diff --git a/python/lib/mrtrix3/dwi2mask/b02template.py b/python/lib/mrtrix3/dwi2mask/b02template.py index b086fe0e7b..bd0badaf3d 100644 --- a/python/lib/mrtrix3/dwi2mask/b02template.py +++ b/python/lib/mrtrix3/dwi2mask/b02template.py @@ -128,7 +128,7 @@ def check_ants_executable(cmdname): check_ants_executable(ANTS_REGISTERFULL_CMD if mode == 'full' else ANTS_REGISTERQUICK_CMD) check_ants_executable(ANTS_TRANSFORM_CMD) if app.ARGS.ants_options: - ants_options_as_path = app.UserPath(app.ARGS.ants_options) + ants_options_as_path = app.Parser.make_userpath_object(app.Parser._UserPathExtras, app.ARGS.ants_options) # pylint: disable=protected-access if ants_options_as_path.is_file(): run.function(shutil.copyfile, ants_options_as_path, 'ants_options.txt') with open('ants_options.txt', 'r', encoding='utf-8') as ants_options_file: diff --git a/python/lib/mrtrix3/fsl.py b/python/lib/mrtrix3/fsl.py index 6bf98914b4..8a57769fba 100644 --- a/python/lib/mrtrix3/fsl.py +++ b/python/lib/mrtrix3/fsl.py @@ -43,7 +43,7 @@ def check_first(prefix, structures): #pylint: disable=unused-variable app.DO_CLEANUP = False raise MRtrixError('FSL FIRST has failed; ' f'{"only " if existing_file_count else ""}{existing_file_count} of {len(vtk_files)} structures were segmented successfully ' - f'(check {app.ScratchPath("first.logs")})') + f'(check {os.path.join(app.SCRATCH_DIR, "first.logs")})') From 45738a23b8e4a64262534945fefa8a66c706517e Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 4 Mar 2024 13:24:45 +1100 Subject: [PATCH 50/75] Python CLI: Default flags to None In the C++ CLI, there are command-line options that themselves receive no arguments. Code typically tests for the mere presence of that option having been specified at the command-line interface. The python argparse module does not offer a direct translation of such. This change modifies the relevant behaviour to be somewhat more faithful, in that if the user does not specify that command-line option, the value stored will be None. (Without this change, typically the value of False is stored. However with the introduction of command-line options that take as input a boolean value, which could be True or False as specified by the user or None if not specified at all, it would be more consistent with such if command-line flags store either True or None, rather than True or False. It also means that app.ARGS can be filtered according to which entries contain None to know what command-line options the user actually specified. --- python/bin/5ttgen | 4 ++-- python/bin/dwicat | 1 + python/bin/dwifslpreproc | 5 +++++ python/bin/for_each | 2 +- python/bin/labelsgmfix | 4 ++-- python/bin/mrtrix_cleanup | 1 + python/bin/population_template | 6 ++++++ python/bin/responsemean | 1 + python/lib/mrtrix3/_5ttgen/fsl.py | 1 + python/lib/mrtrix3/_5ttgen/hsvs.py | 1 + python/lib/mrtrix3/app.py | 7 +++++++ python/lib/mrtrix3/dwi2mask/3dautomask.py | 5 +++++ python/lib/mrtrix3/dwi2mask/fslbet.py | 1 + python/lib/mrtrix3/dwi2mask/hdbet.py | 1 + python/lib/mrtrix3/dwi2mask/synthstrip.py | 4 ++-- python/lib/mrtrix3/dwi2mask/trace.py | 1 + testing/bin/testing_python_cli | 5 ++++- 17 files changed, 42 insertions(+), 8 deletions(-) diff --git a/python/bin/5ttgen b/python/bin/5ttgen index c6e1bc6264..4f95a3eb6d 100755 --- a/python/bin/5ttgen +++ b/python/bin/5ttgen @@ -42,12 +42,12 @@ def usage(cmdline): #pylint: disable=unused-variable common_options = cmdline.add_argument_group('Options common to all 5ttgen algorithms') common_options.add_argument('-nocrop', action='store_true', - default=False, + default=None, help='Do NOT crop the resulting 5TT image to reduce its size ' '(keep the same dimensions as the input image)') common_options.add_argument('-sgm_amyg_hipp', action='store_true', - default=False, + default=None, help='Represent the amygdalae and hippocampi as sub-cortical grey matter in the 5TT image') # Import the command-line settings for all algorithms found in the relevant directory diff --git a/python/bin/dwicat b/python/bin/dwicat index 6612e53d41..586cb86c2e 100755 --- a/python/bin/dwicat +++ b/python/bin/dwicat @@ -60,6 +60,7 @@ def usage(cmdline): #pylint: disable=unused-variable help='Provide a binary mask within which image intensities will be matched') cmdline.add_argument('-nointensity', action='store_true', + default=None, help='Do not perform intensity matching based on b=0 volumes') diff --git a/python/bin/dwifslpreproc b/python/bin/dwifslpreproc index 7cc770a8b7..b1da9c9a69 100755 --- a/python/bin/dwifslpreproc +++ b/python/bin/dwifslpreproc @@ -201,6 +201,7 @@ def usage(cmdline): #pylint: disable=unused-variable ' (i.e. it will not form part of the output image series)') distcorr_options.add_argument('-align_seepi', action='store_true', + default=None, help='Achieve alignment between the SE-EPI images used for inhomogeneity field estimation and the DWIs' ' (more information in Description section)') distcorr_options.add_argument('-topup_options', @@ -245,19 +246,23 @@ def usage(cmdline): #pylint: disable=unused-variable ' note that one of the -rpe_* options MUST be provided') rpe_options.add_argument('-rpe_none', action='store_true', + default=None, help='Specify that no reversed phase-encoding image data is being provided;' ' eddy will perform eddy current and motion correction only') rpe_options.add_argument('-rpe_pair', action='store_true', + default=None, help='Specify that a set of images' ' (typically b=0 volumes)' ' will be provided for use in inhomogeneity field estimation only' ' (using the -se_epi option)') rpe_options.add_argument('-rpe_all', action='store_true', + default=None, help='Specify that ALL DWIs have been acquired with opposing phase-encoding') rpe_options.add_argument('-rpe_header', action='store_true', + default=None, help='Specify that the phase-encoding information can be found in the image header(s),' ' and that this is the information that the script should use') cmdline.flag_mutually_exclusive_options( [ 'rpe_none', 'rpe_pair', 'rpe_all', 'rpe_header' ], True ) diff --git a/python/bin/for_each b/python/bin/for_each index 0931606b8b..d2229dd5ed 100755 --- a/python/bin/for_each +++ b/python/bin/for_each @@ -140,7 +140,7 @@ def usage(cmdline): #pylint: disable=unused-variable '(see Example Usage)') cmdline.add_argument('-test', action='store_true', - default=False, + default=None, help='Test the operation of the for_each script,' ' by printing the command strings following string substitution but not actually executing them') diff --git a/python/bin/labelsgmfix b/python/bin/labelsgmfix index ad8d7de722..33e89126d6 100755 --- a/python/bin/labelsgmfix +++ b/python/bin/labelsgmfix @@ -59,11 +59,11 @@ def usage(cmdline): #pylint: disable=unused-variable help='The output parcellation image') cmdline.add_argument('-premasked', action='store_true', - default=False, + default=None, help='Indicate that brain masking has been applied to the T1 input image') cmdline.add_argument('-sgm_amyg_hipp', action='store_true', - default=False, + default=None, help='Consider the amygdalae and hippocampi as sub-cortical grey matter structures,' ' and also replace their estimates with those from FIRST') diff --git a/python/bin/mrtrix_cleanup b/python/bin/mrtrix_cleanup index a5d210fc04..92956e7039 100755 --- a/python/bin/mrtrix_cleanup +++ b/python/bin/mrtrix_cleanup @@ -46,6 +46,7 @@ def usage(cmdline): #pylint: disable=unused-variable help='Directory from which to commence filesystem search') cmdline.add_argument('-test', action='store_true', + default=None, help='Run script in test mode:' ' will list identified files / directories,' ' but not attempt to delete them') diff --git a/python/bin/population_template b/python/bin/population_template index 61efaa864a..174b4d1812 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -100,9 +100,11 @@ def usage(cmdline): #pylint: disable=unused-variable linoptions = cmdline.add_argument_group('Options for the linear registration') linoptions.add_argument('-linear_no_pause', action='store_true', + default=None, help='Do not pause the script if a linear registration seems implausible') linoptions.add_argument('-linear_no_drift_correction', action='store_true', + default=None, help='Deactivate correction of template appearance (scale and shear) over iterations') linoptions.add_argument('-linear_estimator', choices=LINEAR_ESTIMATORS, @@ -262,6 +264,7 @@ def usage(cmdline): #pylint: disable=unused-variable ' of all subject masks in template space.') options.add_argument('-noreorientation', action='store_true', + default=None, help='Turn off FOD reorientation in mrregister.' ' Reorientation is on by default if the number of volumes in the 4th dimension' ' corresponds to the number of coefficients' @@ -285,14 +288,17 @@ def usage(cmdline): #pylint: disable=unused-variable ' Note that this weighs intensity values not transformations (shape).') options.add_argument('-nanmask', action='store_true', + default=None, help='Optionally apply masks to (transformed) input images using NaN values' ' to specify include areas for registration and aggregation.' ' Only works if -mask_dir has been input.') options.add_argument('-copy_input', action='store_true', + default=None, help='Copy input images and masks into local scratch directory.') options.add_argument('-delete_temporary_files', action='store_true', + default=None, help='Delete temporary files from scratch directory during template creation.') # ENH: add option to initialise warps / transformations diff --git a/python/bin/responsemean b/python/bin/responsemean index d34eaff2d6..6052b98d2c 100755 --- a/python/bin/responsemean +++ b/python/bin/responsemean @@ -47,6 +47,7 @@ def usage(cmdline): #pylint: disable=unused-variable help='The output mean response function file') cmdline.add_argument('-legacy', action='store_true', + default=None, help='Use the legacy behaviour of former command "average_response":' ' average response function coefficients directly,' ' without compensating for global magnitude differences between input files') diff --git a/python/lib/mrtrix3/_5ttgen/fsl.py b/python/lib/mrtrix3/_5ttgen/fsl.py index 4881143f15..d9d400b91d 100644 --- a/python/lib/mrtrix3/_5ttgen/fsl.py +++ b/python/lib/mrtrix3/_5ttgen/fsl.py @@ -56,6 +56,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'rather than deriving one in the script') options.add_argument('-premasked', action='store_true', + default=None, help='Indicate that brain masking has already been applied to the input image') parser.flag_mutually_exclusive_options( [ 'mask', 'premasked' ] ) diff --git a/python/lib/mrtrix3/_5ttgen/hsvs.py b/python/lib/mrtrix3/_5ttgen/hsvs.py index a3178c81c8..72bb0f146c 100644 --- a/python/lib/mrtrix3/_5ttgen/hsvs.py +++ b/python/lib/mrtrix3/_5ttgen/hsvs.py @@ -54,6 +54,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable f'options are: {",".join(THALAMI_CHOICES)}') parser.add_argument('-white_stem', action='store_true', + default=None, help='Classify the brainstem as white matter') parser.add_citation('Smith, R.; Skoch, A.; Bajada, C.; Caspers, S.; Connelly, A. ' 'Hybrid Surface-Volume Segmentation for improved Anatomically-Constrained Tractography. ' diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index fc14d0b632..be1dc25fec 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -875,17 +875,21 @@ def __init__(self, *args_in, **kwargs_in): standard_options = self.add_argument_group('Standard options') standard_options.add_argument('-info', action='store_true', + default=None, help='display information messages.') standard_options.add_argument('-quiet', action='store_true', + default=None, help='do not display information messages or progress status. ' 'Alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string.') standard_options.add_argument('-debug', action='store_true', + default=None, help='display debugging messages.') self.flag_mutually_exclusive_options( [ 'info', 'quiet', 'debug' ] ) standard_options.add_argument('-force', action='store_true', + default=None, help='force overwrite of output files.') standard_options.add_argument('-nthreads', metavar='number', @@ -900,13 +904,16 @@ def __init__(self, *args_in, **kwargs_in): help='temporarily set the value of an MRtrix config file entry.') standard_options.add_argument('-help', action='store_true', + default=None, help='display this information page and exit.') standard_options.add_argument('-version', action='store_true', + default=None, help='display version information and exit.') script_options = self.add_argument_group('Additional standard options for Python scripts') script_options.add_argument('-nocleanup', action='store_true', + default=None, help='do not delete intermediate files during script execution, ' 'and do not delete scratch directory at script completion.') script_options.add_argument('-scratch', diff --git a/python/lib/mrtrix3/dwi2mask/3dautomask.py b/python/lib/mrtrix3/dwi2mask/3dautomask.py index 5f936cbae9..e3bc01279f 100644 --- a/python/lib/mrtrix3/dwi2mask/3dautomask.py +++ b/python/lib/mrtrix3/dwi2mask/3dautomask.py @@ -44,6 +44,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'which will tend to make the mask larger.') options.add_argument('-nograd', action='store_true', + default=None, help='The program uses a "gradual" clip level by default. ' 'Add this option to use a fixed clip level.') options.add_argument('-peels', @@ -58,6 +59,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'It should be between 6 and 26.') options.add_argument('-eclip', action='store_true', + default=None, help='After creating the mask, ' 'remove exterior voxels below the clip threshold.') options.add_argument('-SI', @@ -77,12 +79,15 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options.add_argument('-NN1', action='store_true', + default=None, help='Erode and dilate based on mask faces') options.add_argument('-NN2', action='store_true', + default=None, help='Erode and dilate based on mask edges') options.add_argument('-NN3', action='store_true', + default=None, help='Erode and dilate based on mask corners') diff --git a/python/lib/mrtrix3/dwi2mask/fslbet.py b/python/lib/mrtrix3/dwi2mask/fslbet.py index 4c3bb3ba39..5a39a38450 100644 --- a/python/lib/mrtrix3/dwi2mask/fslbet.py +++ b/python/lib/mrtrix3/dwi2mask/fslbet.py @@ -54,6 +54,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable 'initial surface sphere is set to half of this') options.add_argument('-rescale', action='store_true', + default=None, help='Rescale voxel size provided to BET to 1mm isotropic; ' 'can improve results for rodent data') diff --git a/python/lib/mrtrix3/dwi2mask/hdbet.py b/python/lib/mrtrix3/dwi2mask/hdbet.py index 9b61c231f4..99c63f4a60 100644 --- a/python/lib/mrtrix3/dwi2mask/hdbet.py +++ b/python/lib/mrtrix3/dwi2mask/hdbet.py @@ -39,6 +39,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable options = parser.add_argument_group('Options specific to the "hdbet" algorithm') options.add_argument('-nogpu', action='store_true', + default=None, help='Do not attempt to run on the GPU') diff --git a/python/lib/mrtrix3/dwi2mask/synthstrip.py b/python/lib/mrtrix3/dwi2mask/synthstrip.py index 99e3ee802c..fa270ee20d 100644 --- a/python/lib/mrtrix3/dwi2mask/synthstrip.py +++ b/python/lib/mrtrix3/dwi2mask/synthstrip.py @@ -46,14 +46,14 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable help='The output stripped image') options.add_argument('-gpu', action='store_true', - default=False, + default=None, help='Use the GPU') options.add_argument('-model', type=app.Parser.FileIn(), help='Alternative model weights') options.add_argument('-nocsf', action='store_true', - default=False, + default=None, help='Compute the immediate boundary of brain matter excluding surrounding CSF') options.add_argument('-border', type=app.Parser.Int(), diff --git a/python/lib/mrtrix3/dwi2mask/trace.py b/python/lib/mrtrix3/dwi2mask/trace.py index 0b47727ac1..d03275c19c 100644 --- a/python/lib/mrtrix3/dwi2mask/trace.py +++ b/python/lib/mrtrix3/dwi2mask/trace.py @@ -46,6 +46,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable iter_options = parser.add_argument_group('Options for turning "dwi2mask trace" into an iterative algorithm') iter_options.add_argument('-iterative', action='store_true', + default=None, help='(EXPERIMENTAL) ' 'Iteratively refine the weights for combination of per-shell trace-weighted images ' 'prior to thresholding') diff --git a/testing/bin/testing_python_cli b/testing/bin/testing_python_cli index 38888be58e..f54e571dde 100755 --- a/testing/bin/testing_python_cli +++ b/testing/bin/testing_python_cli @@ -26,6 +26,7 @@ def usage(cmdline): #pylint: disable=unused-variable builtins = cmdline.add_argument_group('Built-in types') builtins.add_argument('-flag', action='store_true', + default=None, help='A binary flag') builtins.add_argument('-string_implicit', help='A built-in string (implicit)') @@ -132,7 +133,9 @@ def execute(): #pylint: disable=unused-variable from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel for key in vars(app.ARGS): - app.console(f'{key}: {repr(getattr(app.ARGS, key))}') + value = getattr(app.ARGS, key) + if value is not None: + app.console(f'{key}: {repr(value)}') From e98ecc60e75a639dfdd2fa90ee78bf2640d94194 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 5 Mar 2024 15:41:22 +1100 Subject: [PATCH 51/75] Testing: Finalise Python CLI testing Updates Python CLI tests proposed as part of #2678 to conform to changes discussed in #2836 and implemented in #2842. --- .gitignore | 4 +++- python/lib/mrtrix3/app.py | 7 +++--- testing/data/python_cli/full_usage.txt | 3 +++ testing/data/python_cli/help.txt | 6 ++--- testing/data/python_cli/markdown.md | 2 ++ testing/data/python_cli/restructured_text.rst | 2 ++ testing/tools/CMakeLists.txt | 1 + testing/unit_tests/CMakeLists.txt | 6 ++++- testing/{tests => unit_tests}/python_cli | 24 +++++++++---------- 9 files changed, 35 insertions(+), 20 deletions(-) rename testing/{tests => unit_tests}/python_cli (56%) diff --git a/.gitignore b/.gitignore index 58b50c3e38..38528667db 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ .check_syntax.tmp .check_syntax2.tmp build/ -CMakeLists.txt.user \ No newline at end of file +CMakeLists.txt.user +testing/data/tmp* +testing/data/*-tmp-* diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index f6dde07932..21aca74c95 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -185,9 +185,10 @@ def _execute(module): #pylint: disable=unused-variable # Now that FORCE_OVERWRITE has been set, # check any user-specified output paths - for arg in vars(ARGS): - if isinstance(arg, Parser._UserOutPathExtras): # pylint: disable=protected-access - arg.check_output() + for key in vars(ARGS): + value = getattr(ARGS, key) + if isinstance(value, Parser._UserOutPathExtras): # pylint: disable=protected-access + value.check_output() # ANSI settings may have been altered at the command-line setup_ansi() diff --git a/testing/data/python_cli/full_usage.txt b/testing/data/python_cli/full_usage.txt index 04821c67b1..da473fffc0 100644 --- a/testing/data/python_cli/full_usage.txt +++ b/testing/data/python_cli/full_usage.txt @@ -50,6 +50,9 @@ ARGUMENT tracks_in 0 0 TRACKSIN OPTION -tracks_out 1 0 An output tractogram ARGUMENT tracks_out 0 0 TRACKSOUT +OPTION -various 1 0 +An option that accepts various types of content +ARGUMENT various 0 0 VARIOUS OPTION -nargs_plus 1 1 A command-line option with nargs="+", no metavar ARGUMENT nargs_plus 0 1 TEXT diff --git a/testing/data/python_cli/help.txt b/testing/data/python_cli/help.txt index 84e1304835..cb8eb76d45 100644 --- a/testing/data/python_cli/help.txt +++ b/testing/data/python_cli/help.txt @@ -1,6 +1,3 @@ -Version 3.0.4-762-gd1588917-dirty tteessttiinngg__ppyytthhoonn__ccllii -using MRtrix3 3.0.4-605-g24f1c322-dirty - tteessttiinngg__ppyytthhoonn__ccllii: external MRtrix3 project SSYYNNOOPPSSIISS @@ -64,6 +61,9 @@ CCuussttoomm  ttyyppeess _-_t_r_a_c_k_s___o_u_t trackfile An output tractogram + _-_v_a_r_i_o_u_s spec + An option that accepts various types of content + CCoommpplleexx  iinntteerrffaacceess;;  nnaarrggss,,  mmeettaavvaarr,,  eettcc.. _-_n_a_r_g_s___p_l_u_s string diff --git a/testing/data/python_cli/markdown.md b/testing/data/python_cli/markdown.md index 1d7cb1b0b5..a99904f42e 100644 --- a/testing/data/python_cli/markdown.md +++ b/testing/data/python_cli/markdown.md @@ -44,6 +44,8 @@ Test operation of the Python command-line interface + **--tracks_out trackfile**
An output tractogram ++ **--various spec**
An option that accepts various types of content + #### Complex interfaces; nargs, metavar, etc. + **--nargs_plus string **
A command-line option with nargs="+", no metavar diff --git a/testing/data/python_cli/restructured_text.rst b/testing/data/python_cli/restructured_text.rst index a5093b98e5..094244ec92 100644 --- a/testing/data/python_cli/restructured_text.rst +++ b/testing/data/python_cli/restructured_text.rst @@ -56,6 +56,8 @@ Custom types - **-tracks_out trackfile** An output tractogram +- **-various spec** An option that accepts various types of content + Complex interfaces; nargs, metavar, etc. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/testing/tools/CMakeLists.txt b/testing/tools/CMakeLists.txt index 588d7a90fa..74496e62c2 100644 --- a/testing/tools/CMakeLists.txt +++ b/testing/tools/CMakeLists.txt @@ -17,6 +17,7 @@ set(CPP_TOOLS_SRCS set(PYTHON_TOOLS_SRCS testing_check_npy testing_gen_npy + testing_python_cli ) function(add_testing_cmd CMD_SRC) diff --git a/testing/unit_tests/CMakeLists.txt b/testing/unit_tests/CMakeLists.txt index 48be163b0a..73b471b006 100644 --- a/testing/unit_tests/CMakeLists.txt +++ b/testing/unit_tests/CMakeLists.txt @@ -13,8 +13,12 @@ set(UNIT_TESTS_CPP_SRCS set(UNIT_TESTS_BASH_SRCS npyread npywrite + python_cli ) +get_filename_component(SOURCE_PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +set(DATA_DIR ${SOURCE_PARENT_DIR}/data) + find_program(BASH bash) function(add_cpp_unit_test FILE_SRC) @@ -44,7 +48,7 @@ function (add_bash_unit_test FILE_SRC) add_bash_tests( FILE_PATH "${FILE_SRC}" PREFIX "unittest" - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + WORKING_DIRECTORY ${DATA_DIR} EXEC_DIRECTORIES "${EXEC_DIRS}" ) endfunction() diff --git a/testing/tests/python_cli b/testing/unit_tests/python_cli similarity index 56% rename from testing/tests/python_cli rename to testing/unit_tests/python_cli index 8561f95fbf..cbdaa8ee64 100644 --- a/testing/tests/python_cli +++ b/testing/unit_tests/python_cli @@ -1,8 +1,8 @@ -mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various -testing_python_cli -help > tmp.txt && diff -q tmp.txt data/python_cli/help.txt -testing_python_cli __print_full_usage__ > tmp.txt && diff -q tmp.txt data/python_cli/full_usage.txt -testing_python_cli __print_usage_markdown__ > tmp.md && diff -q tmp.md data/python_cli/markdown.md -testing_python_cli __print_usage_rst__ > tmp.rst && diff -q tmp.rst data/python_cli/restructured_text.rst +mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck +testing_python_cli -help > tmp.txt && sed -i '1,3d' tmp.txt && diff tmp.txt python_cli/help.txt && rm -f tmp.txt +testing_python_cli __print_full_usage__ > tmp.txt && diff tmp.txt python_cli/full_usage.txt && rm -f tmp.txt +testing_python_cli __print_usage_markdown__ > tmp.md && diff tmp.md python_cli/markdown.md && rm -f tmp.md +testing_python_cli __print_usage_rst__ > tmp.rst && diff tmp.rst python_cli/restructured_text.rst && rm -f tmp.rst testing_python_cli -bool false testing_python_cli -bool False testing_python_cli -bool FALSE @@ -27,12 +27,12 @@ testing_python_cli -int_seq 0.1,0.2,0.3 && false || true testing_python_cli -int_seq Not,An,Int,Seq && false || true testing_python_cli -float_seq Not,A,Float,Seq && false || true rm -rf tmp-dirin/ && testing_python_cli -dir_in tmp-dirin/ && false || true -mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ 2>&1 && grep "use -force to override" -mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ -force +trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force to override" +trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ -force rm -f tmp-filein.txt && testing_python_cli -file_in tmp-filein.txt && false || true -touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt 2>&1 && grep "use -force to override" -touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt -force +trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt 2>&1 | grep -q "use -force to override" +trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt -force rm -f tmp-tracksin.tck && testing_python_cli -tracks_in tmp-tracksin.tck && false || true -touch tmp-filein.txt && testing_python_cli -tracks_in tmp-filein.txt && false || true -touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck 2>&1 && grep "use -force to override" -touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck -force +trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && testing_python_cli -tracks_in tmp-filein.txt && false || true +trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force to override" +trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck -force From 40b7a7c647d33eb30b0c10f82af5cca119167469 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 5 Mar 2024 15:51:01 +1100 Subject: [PATCH 52/75] population_template: Syntax fixes Fix issues with resolution of merge conflicts in 2e6c28a01cd3e4e8f762299af957553cbcb43984. --- python/bin/population_template | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/python/bin/population_template b/python/bin/population_template index 19bcd1ce14..2d9da73f58 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -1484,22 +1484,22 @@ def execute(): #pylint: disable=unused-variable if initial_alignment == 'none' and level == 0: app.console('Calculating average transform from first iteration for linear drift correction') run.command(['transformcalc', - [os.path.join('linear_transforms_{level:%02i}', f'{inp.uid}.txt') for inp in ins], + [os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') for inp in ins], 'average', - 'linear_transform_average_{level:%02i}.txt', + f'linear_transform_average_{level:02d}.txt', '-quiet']) - transform_average_driftref = matrix.load_transform('linear_transform_average_{level:%02i}.txt') + transform_average_driftref = matrix.load_transform(f'linear_transform_average_{level:%02i}.txt') else: run.command(['transformcalc', - [os.path.join('linear_transforms_{level:%02i}', f'{inp.uid}.txt') for inp in ins], + [os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') for inp in ins], 'average', - 'linear_transform_average_{level:%02i}_uncorrected.txt', + f'linear_transform_average_{level:02d}_uncorrected.txt', '-quiet'], force=True) run.command(['transformcalc', - 'linear_transform_average_{level:%02i}_uncorrected.txt', + f'linear_transform_average_{level:02d}_uncorrected.txt', 'invert', - 'linear_transform_average_{level:%02i}_uncorrected_inv.txt', + f'linear_transform_average_{level:02d}_uncorrected_inv.txt', '-quiet'], force=True) transform_average_current_inv = matrix.load_transform(f'linear_transform_average_{level:02d}_uncorrected_inv.txt') From 8a0d639c7732585a38d8cf80ef799d55bfece20a Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 5 Mar 2024 16:05:08 +1100 Subject: [PATCH 53/75] Python CLI: Fix for __print_usage_markdown__ --- python/lib/mrtrix3/app.py | 1 + testing/data/python_cli/markdown.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 21aca74c95..8b9cfd70c6 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -1340,6 +1340,7 @@ def print_group_options(group): for option in group._group_actions: option_text = '/'.join(option.option_strings) option_text += Parser._option2metavar(option) + option_text = option_text.replace("<", "\\<").replace(">", "\\>") group_text += f'+ **-{option_text}**' if isinstance(option, argparse._AppendAction): group_text += ' *(multiple uses permitted)*' diff --git a/testing/data/python_cli/markdown.md b/testing/data/python_cli/markdown.md index a99904f42e..62e78885d6 100644 --- a/testing/data/python_cli/markdown.md +++ b/testing/data/python_cli/markdown.md @@ -48,11 +48,11 @@ Test operation of the Python command-line interface #### Complex interfaces; nargs, metavar, etc. -+ **--nargs_plus string **
A command-line option with nargs="+", no metavar ++ **--nargs_plus string \**
A command-line option with nargs="+", no metavar -+ **--nargs_asterisk **
A command-line option with nargs="*", no metavar ++ **--nargs_asterisk \**
A command-line option with nargs="*", no metavar -+ **--nargs_question **
A command-line option with nargs="?", no metavar ++ **--nargs_question \**
A command-line option with nargs="?", no metavar + **--nargs_two string string**
A command-line option with nargs=2, no metavar From e384c18fb45a1cbd6665d884fb0789fc2918b018 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Tue, 5 Mar 2024 15:27:18 +0000 Subject: [PATCH 54/75] Add argument to specify environment for bash tests This adds a new function argument to add_bash_tests() to specify environment variables that should be defined for running a test. --- cmake/BashTests.cmake | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmake/BashTests.cmake b/cmake/BashTests.cmake index bf86216463..7eda4e3c09 100644 --- a/cmake/BashTests.cmake +++ b/cmake/BashTests.cmake @@ -1,6 +1,6 @@ # A function that adds a bash test for each line in a given file function(add_bash_tests) - set(singleValueArgs FILE_PATH PREFIX WORKING_DIRECTORY) + set(singleValueArgs FILE_PATH PREFIX WORKING_DIRECTORY ENVIRONMENT) set(multiValueArgs EXEC_DIRECTORIES) cmake_parse_arguments( ARG @@ -14,6 +14,7 @@ function(add_bash_tests) set(prefix ${ARG_PREFIX}) set(working_directory ${ARG_WORKING_DIRECTORY}) set(exec_directories ${ARG_EXEC_DIRECTORIES}) + set(environment ${ARG_ENVIRONMENT}) # Regenerate tests when the test script changes set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${file_path}) @@ -67,9 +68,10 @@ function(add_bash_tests) COMMAND ${BASH} -c "export PATH=${EXEC_DIR_PATHS}:$PATH;${line}" WORKING_DIRECTORY ${working_directory} ) - set_tests_properties(${prefix}_${test_name} - PROPERTIES FIXTURES_REQUIRED ${file_name}_cleanup + set_tests_properties(${prefix}_${test_name} PROPERTIES + ENVIRONMENT "${environment}" + FIXTURES_REQUIRED ${file_name}_cleanup ) message(VERBOSE "Add bash tests commands for ${file_name}: ${line}") endforeach() -endfunction() \ No newline at end of file +endfunction() From 7790fd318ab907658a587d8eae6dc0403c0124a1 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 6 Mar 2024 10:40:43 +1100 Subject: [PATCH 55/75] testing: Set PYTHONPATH for non-cpp-standalone unit tests --- testing/unit_tests/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/unit_tests/CMakeLists.txt b/testing/unit_tests/CMakeLists.txt index 73b471b006..84ad5ac939 100644 --- a/testing/unit_tests/CMakeLists.txt +++ b/testing/unit_tests/CMakeLists.txt @@ -50,6 +50,7 @@ function (add_bash_unit_test FILE_SRC) PREFIX "unittest" WORKING_DIRECTORY ${DATA_DIR} EXEC_DIRECTORIES "${EXEC_DIRS}" + ENVIRONMENT "PYTHONPATH=${PROJECT_BINARY_DIR}/lib" ) endfunction() From d9587e650a19c5fc4a54bfac8c654a356d387b13 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 6 Mar 2024 12:30:38 +1100 Subject: [PATCH 56/75] Testing: DO not use sed in Python CLI tests Interface of sed differs between MacOSX and Unix, such that the CI test of only the former would fail. --- testing/data/python_cli/help.txt | 1 + testing/unit_tests/python_cli | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/data/python_cli/help.txt b/testing/data/python_cli/help.txt index cb8eb76d45..d8075efa2f 100644 --- a/testing/data/python_cli/help.txt +++ b/testing/data/python_cli/help.txt @@ -1,3 +1,4 @@ + tteessttiinngg__ppyytthhoonn__ccllii: external MRtrix3 project SSYYNNOOPPSSIISS diff --git a/testing/unit_tests/python_cli b/testing/unit_tests/python_cli index cbdaa8ee64..42a94ce2be 100644 --- a/testing/unit_tests/python_cli +++ b/testing/unit_tests/python_cli @@ -1,5 +1,5 @@ mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck -testing_python_cli -help > tmp.txt && sed -i '1,3d' tmp.txt && diff tmp.txt python_cli/help.txt && rm -f tmp.txt +testing_python_cli -help | tail -n +3 > tmp.txt && diff tmp.txt python_cli/help.txt && rm -f tmp.txt testing_python_cli __print_full_usage__ > tmp.txt && diff tmp.txt python_cli/full_usage.txt && rm -f tmp.txt testing_python_cli __print_usage_markdown__ > tmp.md && diff tmp.md python_cli/markdown.md && rm -f tmp.md testing_python_cli __print_usage_rst__ > tmp.rst && diff tmp.rst python_cli/restructured_text.rst && rm -f tmp.rst From 80658031b3e41a4806d5a6ab9afaa9cfb3eb8805 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 11 Mar 2024 22:05:02 +1100 Subject: [PATCH 57/75] Testing: Add C++ CLI evaluation Tests mirror those added for Python as part of #2678. --- core/app.cpp | 6 +- python/lib/mrtrix3/app.py | 4 +- testing/data/cpp_cli/full_usage.txt | 83 ++++++++++ testing/data/cpp_cli/help.txt | 128 +++++++++++++++ testing/data/cpp_cli/markdown.md | 93 +++++++++++ testing/data/cpp_cli/restructured_text.rst | 104 ++++++++++++ testing/tools/CMakeLists.txt | 1 + testing/tools/testing_cpp_cli.cpp | 174 +++++++++++++++++++++ testing/unit_tests/CMakeLists.txt | 1 + testing/unit_tests/cpp_cli | 38 +++++ testing/unit_tests/python_cli | 8 +- 11 files changed, 630 insertions(+), 10 deletions(-) create mode 100644 testing/data/cpp_cli/full_usage.txt create mode 100644 testing/data/cpp_cli/help.txt create mode 100644 testing/data/cpp_cli/markdown.md create mode 100644 testing/data/cpp_cli/restructured_text.rst create mode 100644 testing/tools/testing_cpp_cli.cpp create mode 100644 testing/unit_tests/cpp_cli diff --git a/core/app.cpp b/core/app.cpp index a4e9b811a3..4fefd1873a 100644 --- a/core/app.cpp +++ b/core/app.cpp @@ -651,10 +651,8 @@ std::string markdown_usage() { s += std::string(REFERENCES[i]) + "\n\n"; s += std::string(MRTRIX_CORE_REFERENCE) + "\n\n"; - s += std::string("---\n\nMRtrix ") + mrtrix_version + ", built " + build_date + - "\n\n" - "\n\n**Author:** " + - AUTHOR + "\n\n**Copyright:** " + COPYRIGHT + "\n\n"; + s += std::string("**Author:** ") + AUTHOR + "\n\n"; + s += std::string("**Copyright:** ") + COPYRIGHT + "\n\n"; return s; } diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 8b9cfd70c6..d53e9006a9 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -605,7 +605,7 @@ def check_output(self, item_type='path'): 'will be overwritten at script completion') else: raise MRtrixError(f'Output {item_type} "{str(self)}" already exists ' - '(use -force to override)') + '(use -force option to force overwrite)') class _UserFileOutPathExtras(_UserOutPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) @@ -631,7 +631,7 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ except FileExistsError: if not FORCE_OVERWRITE: raise MRtrixError(f'Output directory "{str(self)}" already exists ' # pylint: disable=raise-missing-from - '(use -force to override)') + '(use -force option to force overwrite)') # Various callable types for use as argparse argument types class CustomTypeBase: diff --git a/testing/data/cpp_cli/full_usage.txt b/testing/data/cpp_cli/full_usage.txt new file mode 100644 index 0000000000..1e85f1e3bd --- /dev/null +++ b/testing/data/cpp_cli/full_usage.txt @@ -0,0 +1,83 @@ +Verify operation of the C++ command-line interface & parser +OPTION flag 1 0 +An option flag that takes no arguments +OPTION text 1 0 +a text input +ARGUMENT spec 0 0 TEXT +OPTION bool 1 0 +a boolean input +ARGUMENT value 0 0 +OPTION int_unbound 1 0 +an integer input (unbounded) +ARGUMENT value 0 0 INT -9223372036854775808 9223372036854775807 +OPTION int_nonneg 1 0 +a non-negative integer +ARGUMENT value 0 0 INT 0 9223372036854775807 +OPTION int_bound 1 0 +a bound integer +ARGUMENT value 0 0 INT 0 100 +OPTION float_unbound 1 0 +a floating-point number (unbounded) +ARGUMENT value 0 0 FLOAT -inf inf +OPTION float_nonneg 1 0 +a non-negative floating-point number +ARGUMENT value 0 0 FLOAT 0 inf +OPTION float_bound 1 0 +a bound floating-point number +ARGUMENT value 0 0 FLOAT 0 1 +OPTION int_seq 1 0 +a comma-separated sequence of integers +ARGUMENT values 0 0 ISEQ +OPTION float_seq 1 0 +a comma-separated sequence of floating-point numbers +ARGUMENT values 0 0 FSEQ +OPTION choice 1 0 +a choice from a set of options +ARGUMENT item 0 0 CHOICE One Two Three +OPTION file_in 1 0 +an input file +ARGUMENT input 0 0 FILEIN +OPTION file_out 1 0 +an output file +ARGUMENT output 0 0 FILEOUT +OPTION dir_in 1 0 +an input directory +ARGUMENT input 0 0 DIRIN +OPTION dir_out 1 0 +an output directory +ARGUMENT output 0 0 DIROUT +OPTION tracks_in 1 0 +an input tractogram +ARGUMENT input 0 0 TRACKSIN +OPTION tracks_out 1 0 +an output tractogram +ARGUMENT output 0 0 TRACKSOUT +OPTION various 1 0 +an argument that could accept one of various forms +ARGUMENT spec 0 0 VARIOUS +OPTION nargs_two 1 0 +A command-line option that accepts two arguments +ARGUMENT first 0 0 TEXT +ARGUMENT second 0 0 TEXT +OPTION multiple 1 1 +A command-line option that can be specified multiple times +ARGUMENT spec 0 0 TEXT +OPTION info 1 0 +display information messages. +OPTION quiet 1 0 +do not display information messages or progress status; alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string. +OPTION debug 1 0 +display debugging messages. +OPTION force 1 0 +force overwrite of output files (caution: using the same file as input and output might cause unexpected behaviour). +OPTION nthreads 1 0 +use this number of threads in multi-threaded applications (set to 0 to disable multi-threading). +ARGUMENT number 0 0 INT 0 9223372036854775807 +OPTION config 1 1 +temporarily set the value of an MRtrix config file entry. +ARGUMENT key 0 0 TEXT +ARGUMENT value 0 0 TEXT +OPTION help 1 0 +display this information page and exit. +OPTION version 1 0 +display version information and exit. diff --git a/testing/data/cpp_cli/help.txt b/testing/data/cpp_cli/help.txt new file mode 100644 index 0000000000..ba1edfc542 --- /dev/null +++ b/testing/data/cpp_cli/help.txt @@ -0,0 +1,128 @@ + tteessttiinngg__ccpppp__ccllii: part of the MRtrix3 package + +SSYYNNOOPPSSIISS + + Verify operation of the C++ command-line interface & parser + +UUSSAAGGEE + + _t_e_s_t_i_n_g___c_p_p___c_l_i [ options ] + + +OOPPTTIIOONNSS + + _-_f_l_a_g + An option flag that takes no arguments + + _-_t_e_x_t spec + a text input + + _-_b_o_o_l value + a boolean input + + _-_i_n_t___u_n_b_o_u_n_d value + an integer input (unbounded) + + _-_i_n_t___n_o_n_n_e_g value + a non-negative integer + + _-_i_n_t___b_o_u_n_d value + a bound integer + + _-_f_l_o_a_t___u_n_b_o_u_n_d value + a floating-point number (unbounded) + + _-_f_l_o_a_t___n_o_n_n_e_g value + a non-negative floating-point number + + _-_f_l_o_a_t___b_o_u_n_d value + a bound floating-point number + + _-_i_n_t___s_e_q values + a comma-separated sequence of integers + + _-_f_l_o_a_t___s_e_q values + a comma-separated sequence of floating-point numbers + + _-_c_h_o_i_c_e item + a choice from a set of options + + _-_f_i_l_e___i_n input + an input file + + _-_f_i_l_e___o_u_t output + an output file + + _-_d_i_r___i_n input + an input directory + + _-_d_i_r___o_u_t output + an output directory + + _-_t_r_a_c_k_s___i_n input + an input tractogram + + _-_t_r_a_c_k_s___o_u_t output + an output tractogram + + _-_v_a_r_i_o_u_s spec + an argument that could accept one of various forms + + _-_n_a_r_g_s___t_w_o first second + A command-line option that accepts two arguments + + _-_m_u_l_t_i_p_l_e spec (multiple uses permitted) + A command-line option that can be specified multiple times + +SSttaannddaarrdd  ooppttiioonnss + + _-_i_n_f_o + display information messages. + + _-_q_u_i_e_t + do not display information messages or progress status; alternatively, + this can be achieved by setting the MRTRIX_QUIET environment variable to a + non-empty string. + + _-_d_e_b_u_g + display debugging messages. + + _-_f_o_r_c_e + force overwrite of output files (caution: using the same file as input and + output might cause unexpected behaviour). + + _-_n_t_h_r_e_a_d_s number + use this number of threads in multi-threaded applications (set to 0 to + disable multi-threading). + + _-_c_o_n_f_i_g key value (multiple uses permitted) + temporarily set the value of an MRtrix config file entry. + + _-_h_e_l_p + display this information page and exit. + + _-_v_e_r_s_i_o_n + display version information and exit. + +AAUUTTHHOORR + Robert E. Smith (robert.smith@florey.edu.au) + +CCOOPPYYRRIIGGHHTT + Copyright (c) 2008-2024 the MRtrix3 contributors. + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + Covered Software is provided under this License on an "as is" + basis, without warranty of any kind, either expressed, implied, or + statutory, including, without limitation, warranties that the + Covered Software is free of defects, merchantable, fit for a + particular purpose or non-infringing. + See the Mozilla Public License v. 2.0 for more details. + For more details, see http://www.mrtrix.org/. + +RREEFFEERREENNCCEESS + Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; + Pietsch, M.; Christiaens, D.; Jeurissen, B.; Yeh, C.-H. & Connelly, A. + MRtrix3: A fast, flexible and open software framework for medical image + processing and visualisation. NeuroImage, 2019, 202, 116137 + diff --git a/testing/data/cpp_cli/markdown.md b/testing/data/cpp_cli/markdown.md new file mode 100644 index 0000000000..a5f0b63c30 --- /dev/null +++ b/testing/data/cpp_cli/markdown.md @@ -0,0 +1,93 @@ +## Synopsis + +Verify operation of the C++ command-line interface & parser + +## Usage + + testing_cpp_cli [ options ] + + +## Options + ++ **-flag**
An option flag that takes no arguments + ++ **-text spec**
a text input + ++ **-bool value**
a boolean input + ++ **-int_unbound value**
an integer input (unbounded) + ++ **-int_nonneg value**
a non-negative integer + ++ **-int_bound value**
a bound integer + ++ **-float_unbound value**
a floating-point number (unbounded) + ++ **-float_nonneg value**
a non-negative floating-point number + ++ **-float_bound value**
a bound floating-point number + ++ **-int_seq values**
a comma-separated sequence of integers + ++ **-float_seq values**
a comma-separated sequence of floating-point numbers + ++ **-choice item**
a choice from a set of options + ++ **-file_in input**
an input file + ++ **-file_out output**
an output file + ++ **-dir_in input**
an input directory + ++ **-dir_out output**
an output directory + ++ **-tracks_in input**
an input tractogram + ++ **-tracks_out output**
an output tractogram + ++ **-various spec**
an argument that could accept one of various forms + ++ **-nargs_two first second**
A command-line option that accepts two arguments + ++ **-multiple spec** *(multiple uses permitted)*
A command-line option that can be specified multiple times + +#### Standard options + ++ **-info**
display information messages. + ++ **-quiet**
do not display information messages or progress status; alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string. + ++ **-debug**
display debugging messages. + ++ **-force**
force overwrite of output files (caution: using the same file as input and output might cause unexpected behaviour). + ++ **-nthreads number**
use this number of threads in multi-threaded applications (set to 0 to disable multi-threading). + ++ **-config key value** *(multiple uses permitted)*
temporarily set the value of an MRtrix config file entry. + ++ **-help**
display this information page and exit. + ++ **-version**
display version information and exit. + +## References + +Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; Pietsch, M.; Christiaens, D.; Jeurissen, B.; Yeh, C.-H. & Connelly, A. MRtrix3: A fast, flexible and open software framework for medical image processing and visualisation. NeuroImage, 2019, 202, 116137 + +**Author:** Robert E. Smith (robert.smith@florey.edu.au) + +**Copyright:** Copyright (c) 2008-2024 the MRtrix3 contributors. + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Covered Software is provided under this License on an "as is" +basis, without warranty of any kind, either expressed, implied, or +statutory, including, without limitation, warranties that the +Covered Software is free of defects, merchantable, fit for a +particular purpose or non-infringing. +See the Mozilla Public License v. 2.0 for more details. + +For more details, see http://www.mrtrix.org/. + + diff --git a/testing/data/cpp_cli/restructured_text.rst b/testing/data/cpp_cli/restructured_text.rst new file mode 100644 index 0000000000..41f0e72662 --- /dev/null +++ b/testing/data/cpp_cli/restructured_text.rst @@ -0,0 +1,104 @@ +Synopsis +-------- + +Verify operation of the C++ command-line interface & parser + +Usage +-------- + +:: + + testing_cpp_cli [ options ] + + +Options +------- + +- **-flag** An option flag that takes no arguments + +- **-text spec** a text input + +- **-bool value** a boolean input + +- **-int_unbound value** an integer input (unbounded) + +- **-int_nonneg value** a non-negative integer + +- **-int_bound value** a bound integer + +- **-float_unbound value** a floating-point number (unbounded) + +- **-float_nonneg value** a non-negative floating-point number + +- **-float_bound value** a bound floating-point number + +- **-int_seq values** a comma-separated sequence of integers + +- **-float_seq values** a comma-separated sequence of floating-point numbers + +- **-choice item** a choice from a set of options + +- **-file_in input** an input file + +- **-file_out output** an output file + +- **-dir_in input** an input directory + +- **-dir_out output** an output directory + +- **-tracks_in input** an input tractogram + +- **-tracks_out output** an output tractogram + +- **-various spec** an argument that could accept one of various forms + +- **-nargs_two first second** A command-line option that accepts two arguments + +- **-multiple spec** *(multiple uses permitted)* A command-line option that can be specified multiple times + +Standard options +^^^^^^^^^^^^^^^^ + +- **-info** display information messages. + +- **-quiet** do not display information messages or progress status; alternatively, this can be achieved by setting the MRTRIX_QUIET environment variable to a non-empty string. + +- **-debug** display debugging messages. + +- **-force** force overwrite of output files (caution: using the same file as input and output might cause unexpected behaviour). + +- **-nthreads number** use this number of threads in multi-threaded applications (set to 0 to disable multi-threading). + +- **-config key value** *(multiple uses permitted)* temporarily set the value of an MRtrix config file entry. + +- **-help** display this information page and exit. + +- **-version** display version information and exit. + +References +^^^^^^^^^^ + +Tournier, J.-D.; Smith, R. E.; Raffelt, D.; Tabbara, R.; Dhollander, T.; Pietsch, M.; Christiaens, D.; Jeurissen, B.; Yeh, C.-H. & Connelly, A. MRtrix3: A fast, flexible and open software framework for medical image processing and visualisation. NeuroImage, 2019, 202, 116137 + +-------------- + + + +**Author:** Robert E. Smith (robert.smith@florey.edu.au) + +**Copyright:** Copyright (c) 2008-2024 the MRtrix3 contributors. + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Covered Software is provided under this License on an "as is" +basis, without warranty of any kind, either expressed, implied, or +statutory, including, without limitation, warranties that the +Covered Software is free of defects, merchantable, fit for a +particular purpose or non-infringing. +See the Mozilla Public License v. 2.0 for more details. + +For more details, see http://www.mrtrix.org/. + + diff --git a/testing/tools/CMakeLists.txt b/testing/tools/CMakeLists.txt index 74496e62c2..234ea5e794 100644 --- a/testing/tools/CMakeLists.txt +++ b/testing/tools/CMakeLists.txt @@ -1,4 +1,5 @@ set(CPP_TOOLS_SRCS + testing_cpp_cli.cpp testing_diff_dir.cpp testing_diff_fixel.cpp testing_diff_fixel_old.cpp diff --git a/testing/tools/testing_cpp_cli.cpp b/testing/tools/testing_cpp_cli.cpp new file mode 100644 index 0000000000..81cbda00b4 --- /dev/null +++ b/testing/tools/testing_cpp_cli.cpp @@ -0,0 +1,174 @@ +/* Copyright (c) 2008-2024 the MRtrix3 contributors. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Covered Software is provided under this License on an "as is" + * basis, without warranty of any kind, either expressed, implied, or + * statutory, including, without limitation, warranties that the + * Covered Software is free of defects, merchantable, fit for a + * particular purpose or non-infringing. + * See the Mozilla Public License v. 2.0 for more details. + * + * For more details, see http://www.mrtrix.org/. + */ + +#include +#include + +#include "command.h" + +using namespace MR; +using namespace App; + +const char* const choices[] = { "One", "Two", "Three", nullptr }; + +// clang-format off +void usage() { + + AUTHOR = "Robert E. Smith (robert.smith@florey.edu.au)"; + + SYNOPSIS = "Verify operation of the C++ command-line interface & parser"; + + REQUIRES_AT_LEAST_ONE_ARGUMENT = false; + + OPTIONS + + Option("flag", "An option flag that takes no arguments") + + + Option("text", "a text input") + + Argument("spec").type_text() + + + Option("bool", "a boolean input") + + Argument("value").type_bool() + + + Option("int_unbound", "an integer input (unbounded)") + + Argument("value").type_integer() + + + Option("int_nonneg", "a non-negative integer") + + Argument("value").type_integer(0) + + + Option("int_bound", "a bound integer") + + Argument("value").type_integer(0, 100) + + + Option("float_unbound", "a floating-point number (unbounded)") + + Argument("value").type_float() + + + Option("float_nonneg", "a non-negative floating-point number") + + Argument("value").type_float(0.0) + + + Option("float_bound", "a bound floating-point number") + + Argument("value").type_float(0.0, 1.0) + + + Option("int_seq", "a comma-separated sequence of integers") + + Argument("values").type_sequence_int() + + + Option("float_seq", "a comma-separated sequence of floating-point numbers") + + Argument("values").type_sequence_float() + + + Option("choice", "a choice from a set of options") + + Argument("item").type_choice(choices) + + + Option("file_in", "an input file") + + Argument("input").type_file_in() + + + Option("file_out", "an output file") + + Argument("output").type_file_out() + + + Option("dir_in", "an input directory") + + Argument("input").type_directory_in() + + + Option("dir_out", "an output directory") + + Argument("output").type_directory_out() + + + Option("tracks_in", "an input tractogram") + + Argument("input").type_tracks_in() + + + Option("tracks_out", "an output tractogram") + + Argument("output").type_tracks_out() + + + Option("various", "an argument that could accept one of various forms") + + Argument("spec").type_various() + + + Option("nargs_two", "A command-line option that accepts two arguments") + + Argument("first").type_text() + + Argument("second").type_text() + + + Option("multiple", "A command-line option that can be specified multiple times").allow_multiple() + + Argument("spec").type_text(); + +} +// clang-format on + +void run() { + + if (!get_options("flag").empty()) + CONSOLE("-flag option present"); + + auto opt = get_options("text"); + if (!opt.empty()) + CONSOLE("-text: " + std::string(opt[0][0])); + opt = get_options("bool"); + if (!opt.empty()) + CONSOLE("-bool: " + str(bool(opt[0][0]))); + opt = get_options("int_unbound"); + if (!opt.empty()) + CONSOLE("-int_unbound: " + str(int64_t(opt[0][0]))); + opt = get_options("int_nonneg"); + if (!opt.empty()) + CONSOLE("-int_nonneg: " + str(int64_t(opt[0][0]))); + opt = get_options("int_bound"); + if (!opt.empty()) + CONSOLE("-int_bound: " + str(int64_t(opt[0][0]))); + opt = get_options("float_unbound"); + if (!opt.empty()) + CONSOLE("-float_unbound: " + str(default_type(opt[0][0]))); + opt = get_options("float_nonneg"); + if (!opt.empty()) + CONSOLE("-float_nonneg: " + str(default_type(opt[0][0]))); + opt = get_options("float_bound"); + if (!opt.empty()) + CONSOLE("-float_bound: " + str(default_type(opt[0][0]))); + opt = get_options("int_seq"); + if (!opt.empty()) + CONSOLE("-int_seq: [" + join(parse_ints(opt[0][0]), ",") + "]"); + opt = get_options("float_seq"); + if (!opt.empty()) + CONSOLE("-float_seq: [" + join(parse_floats(opt[0][0]), ",") + "]"); + opt = get_options("choice"); + if (!opt.empty()) + CONSOLE("-choice: " + str(opt[0][0])); + opt = get_options("file_in"); + if (!opt.empty()) + CONSOLE("-file_in: " + str(opt[0][0])); + opt = get_options("file_out"); + if (!opt.empty()) + CONSOLE("-file_out: " + str(opt[0][0])); + opt = get_options("dir_in"); + if (!opt.empty()) + CONSOLE("-dir_in: " + str(opt[0][0])); + opt = get_options("dir_out"); + if (!opt.empty()) + CONSOLE("-dir_out: " + str(opt[0][0])); + opt = get_options("tracks_in"); + if (!opt.empty()) + CONSOLE("-tracks_in: " + str(opt[0][0])); + opt = get_options("tracks_out"); + if (!opt.empty()) + CONSOLE("-tracks_out: " + str(opt[0][0])); + + opt = get_options("various"); + if (!opt.empty()) + CONSOLE("-various: " + str(opt[0][0])); + opt = get_options("nargs_two"); + if (!opt.empty()) + CONSOLE("-nargs_two: [" + str(opt[0][0]) + " " + str(opt[0][1]) + "]"); + opt = get_options("multiple"); + if (!opt.empty()) { + std::vector specs; + for (size_t i = 0; i != opt.size(); ++i) + specs.push_back (std::string("\"") + str(opt[i][0]) + "\""); + CONSOLE("-multiple: [" + join(specs, " ") + "]"); + } + +} diff --git a/testing/unit_tests/CMakeLists.txt b/testing/unit_tests/CMakeLists.txt index 73b471b006..05040d841e 100644 --- a/testing/unit_tests/CMakeLists.txt +++ b/testing/unit_tests/CMakeLists.txt @@ -11,6 +11,7 @@ set(UNIT_TESTS_CPP_SRCS ) set(UNIT_TESTS_BASH_SRCS + cpp_cli npyread npywrite python_cli diff --git a/testing/unit_tests/cpp_cli b/testing/unit_tests/cpp_cli new file mode 100644 index 0000000000..eaf7f14a49 --- /dev/null +++ b/testing/unit_tests/cpp_cli @@ -0,0 +1,38 @@ +mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_cpp_cli -flag -text my_text -choice One -bool false -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck +testing_cpp_cli -help > tmp.txt && sed -i '1,2d' tmp.txt && diff tmp.txt cpp_cli/help.txt && rm -f tmp.txt +testing_cpp_cli __print_full_usage__ > tmp.txt && diff tmp.txt cpp_cli/full_usage.txt && rm -f tmp.txt +testing_cpp_cli __print_usage_markdown__ > tmp.md && diff tmp.md cpp_cli/markdown.md && rm -f tmp.md +testing_cpp_cli __print_usage_rst__ > tmp.rst && diff tmp.rst cpp_cli/restructured_text.rst && rm -f tmp.rst +testing_cpp_cli -bool false +testing_cpp_cli -bool False +testing_cpp_cli -bool FALSE +testing_cpp_cli -bool true +testing_cpp_cli -bool True +testing_cpp_cli -bool TRUE +testing_cpp_cli -bool 0 +testing_cpp_cli -bool 1 +testing_cpp_cli -bool 2 +testing_cpp_cli -bool NotABool && false || true +testing_cpp_cli -int_builtin 0.1 && false || true +testing_cpp_cli -int_builtin NotAnInt && false || true +testing_cpp_cli -int_unbound 0.1 && false || true +testing_cpp_cli -int_unbound NotAnInt && false || true +testing_cpp_cli -int_nonneg -1 && false || true +testing_cpp_cli -int_bound 101 && false || true +testing_cpp_cli -float_builtin NotAFloat && false || true +testing_cpp_cli -float_unbound NotAFloat && false || true +testing_cpp_cli -float_nonneg -0.1 && false || true +testing_cpp_cli -float_bound 1.1 && false || true +testing_cpp_cli -int_seq 0.1,0.2,0.3 && false || true +testing_cpp_cli -int_seq Not,An,Int,Seq && false || true +testing_cpp_cli -float_seq Not,A,Float,Seq && false || true +rm -rf tmp-dirin/ && testing_cpp_cli -dir_in tmp-dirin/ && false || true +trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_cpp_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force option to force overwrite" +trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_cpp_cli -dir_out tmp-dirout/ -force +rm -f tmp-filein.txt && testing_cpp_cli -file_in tmp-filein.txt && false || true +trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_cpp_cli -file_out tmp-fileout.txt 2>&1 | grep -q "use -force option to force overwrite" +trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_cpp_cli -file_out tmp-fileout.txt -force +rm -f tmp-tracksin.tck && testing_cpp_cli -tracks_in tmp-tracksin.tck && false || true +trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && testing_cpp_cli -tracks_in tmp-filein.txt && false || true +trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_cpp_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force option to force overwrite" +trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_cpp_cli -tracks_out tmp-tracksout.tck -force diff --git a/testing/unit_tests/python_cli b/testing/unit_tests/python_cli index cbdaa8ee64..955dc7fac1 100644 --- a/testing/unit_tests/python_cli +++ b/testing/unit_tests/python_cli @@ -1,4 +1,4 @@ -mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck +mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -flag -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck testing_python_cli -help > tmp.txt && sed -i '1,3d' tmp.txt && diff tmp.txt python_cli/help.txt && rm -f tmp.txt testing_python_cli __print_full_usage__ > tmp.txt && diff tmp.txt python_cli/full_usage.txt && rm -f tmp.txt testing_python_cli __print_usage_markdown__ > tmp.md && diff tmp.md python_cli/markdown.md && rm -f tmp.md @@ -27,12 +27,12 @@ testing_python_cli -int_seq 0.1,0.2,0.3 && false || true testing_python_cli -int_seq Not,An,Int,Seq && false || true testing_python_cli -float_seq Not,A,Float,Seq && false || true rm -rf tmp-dirin/ && testing_python_cli -dir_in tmp-dirin/ && false || true -trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force to override" +trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force option to force overwrite" trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ -force rm -f tmp-filein.txt && testing_python_cli -file_in tmp-filein.txt && false || true -trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt 2>&1 | grep -q "use -force to override" +trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt 2>&1 | grep -q "use -force option to force overwrite" trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt -force rm -f tmp-tracksin.tck && testing_python_cli -tracks_in tmp-tracksin.tck && false || true trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && testing_python_cli -tracks_in tmp-filein.txt && false || true -trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force to override" +trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force option to force overwrite" trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck -force From be756183f4d21058d0f6b7a950013d1f0e8c6dda Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 12 Mar 2024 08:43:47 +1100 Subject: [PATCH 58/75] CI: Fix PYTHONPATH setting in MSYS2 Co-authored-by: Daljit Singh --- testing/unit_tests/CMakeLists.txt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/testing/unit_tests/CMakeLists.txt b/testing/unit_tests/CMakeLists.txt index 84ad5ac939..32755c7620 100644 --- a/testing/unit_tests/CMakeLists.txt +++ b/testing/unit_tests/CMakeLists.txt @@ -21,6 +21,17 @@ set(DATA_DIR ${SOURCE_PARENT_DIR}/data) find_program(BASH bash) +set(PYTHON_ENV_PATH "${PROJECT_BINARY_DIR}/lib") +# On MSYS2 we need to convert Windows paths to Unix paths +if(MINGW AND WIN32) + EXECUTE_PROCESS( + COMMAND cygpath -u ${PYTHON_ENV_PATH} + OUTPUT_VARIABLE PYTHON_ENV_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif() + + function(add_cpp_unit_test FILE_SRC) get_filename_component(NAME ${FILE_SRC} NAME_WE) add_executable(${NAME} ${FILE_SRC}) @@ -50,7 +61,7 @@ function (add_bash_unit_test FILE_SRC) PREFIX "unittest" WORKING_DIRECTORY ${DATA_DIR} EXEC_DIRECTORIES "${EXEC_DIRS}" - ENVIRONMENT "PYTHONPATH=${PROJECT_BINARY_DIR}/lib" + ENVIRONMENT "PYTHONPATH=${PYTHON_ENV_PATH}" ) endfunction() From 08bc526c324c22d450c0fde87c8b064b455e3d11 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 12 Mar 2024 08:52:05 +1100 Subject: [PATCH 59/75] CI: Fix CLI dunder function checks on MSYS2 --- testing/unit_tests/python_cli | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/unit_tests/python_cli b/testing/unit_tests/python_cli index 42a94ce2be..ff40e49c70 100644 --- a/testing/unit_tests/python_cli +++ b/testing/unit_tests/python_cli @@ -1,8 +1,8 @@ mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck -testing_python_cli -help | tail -n +3 > tmp.txt && diff tmp.txt python_cli/help.txt && rm -f tmp.txt -testing_python_cli __print_full_usage__ > tmp.txt && diff tmp.txt python_cli/full_usage.txt && rm -f tmp.txt -testing_python_cli __print_usage_markdown__ > tmp.md && diff tmp.md python_cli/markdown.md && rm -f tmp.md -testing_python_cli __print_usage_rst__ > tmp.rst && diff tmp.rst python_cli/restructured_text.rst && rm -f tmp.rst +testing_python_cli -help | tail -n +3 > tmp.txt && diff -a --strip-trailing-cr tmp.txt python_cli/help.txt && rm -f tmp.txt +testing_python_cli __print_full_usage__ > tmp.txt && diff -a --strip-trailing-cr tmp.txt python_cli/full_usage.txt && rm -f tmp.txt +testing_python_cli __print_usage_markdown__ > tmp.md && diff -a --strip-trailing-cr tmp.md python_cli/markdown.md && rm -f tmp.md +testing_python_cli __print_usage_rst__ > tmp.rst && diff -a --strip-trailing-cr tmp.rst python_cli/restructured_text.rst && rm -f tmp.rst testing_python_cli -bool false testing_python_cli -bool False testing_python_cli -bool FALSE From 4c9247b8028916ebf2b2bf17a952a1d590ab5f31 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 12 Mar 2024 09:27:40 +1100 Subject: [PATCH 60/75] CI: Fix C++ CLI tests on MSYS2 Echoes changes in 08bc526c324c22d450c0fde87c8b064b455e3d11 and d9587e650a19c5fc4a54bfac8c654a356d387b13 for the Python CLI tests. --- testing/unit_tests/cpp_cli | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/unit_tests/cpp_cli b/testing/unit_tests/cpp_cli index eaf7f14a49..a346ce0e19 100644 --- a/testing/unit_tests/cpp_cli +++ b/testing/unit_tests/cpp_cli @@ -1,8 +1,8 @@ mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_cpp_cli -flag -text my_text -choice One -bool false -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck -testing_cpp_cli -help > tmp.txt && sed -i '1,2d' tmp.txt && diff tmp.txt cpp_cli/help.txt && rm -f tmp.txt -testing_cpp_cli __print_full_usage__ > tmp.txt && diff tmp.txt cpp_cli/full_usage.txt && rm -f tmp.txt -testing_cpp_cli __print_usage_markdown__ > tmp.md && diff tmp.md cpp_cli/markdown.md && rm -f tmp.md -testing_cpp_cli __print_usage_rst__ > tmp.rst && diff tmp.rst cpp_cli/restructured_text.rst && rm -f tmp.rst +testing_cpp_cli -help | tail -n +2 > tmp.txt && diff -a --strip-trailing-cr tmp.txt cpp_cli/help.txt && rm -f tmp.txt +testing_cpp_cli __print_full_usage__ > tmp.txt && diff -a --strip-trailing-cr tmp.txt cpp_cli/full_usage.txt && rm -f tmp.txt +testing_cpp_cli __print_usage_markdown__ > tmp.md && diff -a --strip-trailing-cr tmp.md cpp_cli/markdown.md && rm -f tmp.md +testing_cpp_cli __print_usage_rst__ > tmp.rst && diff -a --strip-trailing-cr tmp.rst cpp_cli/restructured_text.rst && rm -f tmp.rst testing_cpp_cli -bool false testing_cpp_cli -bool False testing_cpp_cli -bool FALSE From d6a84bad15b850f9c005c9363addda9f31143bfa Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 12 Mar 2024 11:37:37 +1100 Subject: [PATCH 61/75] CI: Final fixes for addition of C++ CLI tests --- testing/tools/testing_cpp_cli.cpp | 5 ++--- testing/unit_tests/cpp_cli | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/testing/tools/testing_cpp_cli.cpp b/testing/tools/testing_cpp_cli.cpp index 81cbda00b4..310a1011c8 100644 --- a/testing/tools/testing_cpp_cli.cpp +++ b/testing/tools/testing_cpp_cli.cpp @@ -22,7 +22,7 @@ using namespace MR; using namespace App; -const char* const choices[] = { "One", "Two", "Three", nullptr }; +const char *const choices[] = {"One", "Two", "Three", nullptr}; // clang-format off void usage() { @@ -167,8 +167,7 @@ void run() { if (!opt.empty()) { std::vector specs; for (size_t i = 0; i != opt.size(); ++i) - specs.push_back (std::string("\"") + str(opt[i][0]) + "\""); + specs.push_back(std::string("\"") + str(opt[i][0]) + "\""); CONSOLE("-multiple: [" + join(specs, " ") + "]"); } - } diff --git a/testing/unit_tests/cpp_cli b/testing/unit_tests/cpp_cli index a346ce0e19..2aa9fdcc52 100644 --- a/testing/unit_tests/cpp_cli +++ b/testing/unit_tests/cpp_cli @@ -1,5 +1,5 @@ mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_cpp_cli -flag -text my_text -choice One -bool false -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck -testing_cpp_cli -help | tail -n +2 > tmp.txt && diff -a --strip-trailing-cr tmp.txt cpp_cli/help.txt && rm -f tmp.txt +testing_cpp_cli -help | tail -n +3 > tmp.txt && diff -a --strip-trailing-cr tmp.txt cpp_cli/help.txt && rm -f tmp.txt testing_cpp_cli __print_full_usage__ > tmp.txt && diff -a --strip-trailing-cr tmp.txt cpp_cli/full_usage.txt && rm -f tmp.txt testing_cpp_cli __print_usage_markdown__ > tmp.md && diff -a --strip-trailing-cr tmp.md cpp_cli/markdown.md && rm -f tmp.md testing_cpp_cli __print_usage_rst__ > tmp.rst && diff -a --strip-trailing-cr tmp.rst cpp_cli/restructured_text.rst && rm -f tmp.rst From 65ba8562b59e5c3c9ee15374bd662ccbf43f5068 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 13 May 2024 21:28:38 +1000 Subject: [PATCH 62/75] Testing: Greater testing of Python image piping support --- testing/scripts/CMakeLists.txt | 49 +++++++++++++++++-- .../scripts/tests/dwi2mask/3dautomask_piping | 7 +++ testing/scripts/tests/dwi2mask/ants_piping | 22 +++++++++ testing/scripts/tests/dwi2mask/consensus | 10 ---- .../scripts/tests/dwi2mask/consensus_default | 9 ++++ .../scripts/tests/dwi2mask/consensus_piping | 27 ++++++++++ .../scripts/tests/dwi2mask/consensus_template | 11 +++++ testing/scripts/tests/dwi2mask/fslbet_piping | 7 +++ .../tests/dwi2mask/{hdbet => hdbet_default} | 1 + testing/scripts/tests/dwi2mask/hdbet_piping | 7 +++ .../tests/dwi2mask/{mean => mean_default} | 1 + testing/scripts/tests/dwi2mask/mean_piping | 7 +++ .../scripts/tests/dwi2mask/mtnorm_initmask | 2 +- testing/scripts/tests/dwi2mask/trace_piping | 7 +++ testing/scripts/tests/dwibiasnormmask/piping | 28 +++++++++++ testing/scripts/tests/labelsgmfirst/piping | 13 +++++ testing/scripts/tests/mask2glass/default | 5 ++ testing/scripts/tests/mask2glass/piping | 6 +++ testing/scripts/tests/mrtrix_cleanup/default | 16 ++++++ testing/scripts/tests/mrtrix_cleanup/test | 15 ++++++ 20 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 testing/scripts/tests/dwi2mask/3dautomask_piping create mode 100644 testing/scripts/tests/dwi2mask/ants_piping delete mode 100644 testing/scripts/tests/dwi2mask/consensus create mode 100644 testing/scripts/tests/dwi2mask/consensus_default create mode 100644 testing/scripts/tests/dwi2mask/consensus_piping create mode 100644 testing/scripts/tests/dwi2mask/consensus_template create mode 100644 testing/scripts/tests/dwi2mask/fslbet_piping rename testing/scripts/tests/dwi2mask/{hdbet => hdbet_default} (88%) create mode 100644 testing/scripts/tests/dwi2mask/hdbet_piping rename testing/scripts/tests/dwi2mask/{mean => mean_default} (89%) create mode 100644 testing/scripts/tests/dwi2mask/mean_piping create mode 100644 testing/scripts/tests/dwi2mask/trace_piping create mode 100644 testing/scripts/tests/dwibiasnormmask/piping create mode 100644 testing/scripts/tests/labelsgmfirst/piping create mode 100644 testing/scripts/tests/mask2glass/default create mode 100644 testing/scripts/tests/mask2glass/piping create mode 100644 testing/scripts/tests/mrtrix_cleanup/default create mode 100644 testing/scripts/tests/mrtrix_cleanup/test diff --git a/testing/scripts/CMakeLists.txt b/testing/scripts/CMakeLists.txt index c1ee3406bf..a32ebbdcde 100644 --- a/testing/scripts/CMakeLists.txt +++ b/testing/scripts/CMakeLists.txt @@ -31,22 +31,27 @@ endfunction() add_bash_script_test(5ttgen/freesurfer_default "pythonci") add_bash_script_test(5ttgen/freesurfer_nocrop) +add_bash_script_test(5ttgen/freesurfer_piping) add_bash_script_test(5ttgen/freesurfer_sgmamyghipp) add_bash_script_test(5ttgen/fsl_default) add_bash_script_test(5ttgen/fsl_mask) add_bash_script_test(5ttgen/fsl_nocrop) +add_bash_script_test(5ttgen/fsl_piping) add_bash_script_test(5ttgen/fsl_premasked) add_bash_script_test(5ttgen/fsl_sgmamyghipp) add_bash_script_test(5ttgen/hsvs_aseg) add_bash_script_test(5ttgen/hsvs_default) add_bash_script_test(5ttgen/hsvs_first) add_bash_script_test(5ttgen/hsvs_modules) +add_bash_script_test(5ttgen/hsvs_piping) add_bash_script_test(5ttgen/hsvs_template) add_bash_script_test(5ttgen/hsvs_whitestem) add_bash_script_test(dwi2mask/3dautomask_default) add_bash_script_test(dwi2mask/3dautomask_options) +add_bash_script_test(dwi2mask/3dautomask_piping) add_bash_script_test(dwi2mask/ants_config) +add_bash_script_test(dwi2mask/ants_piping) add_bash_script_test(dwi2mask/ants_template) add_bash_script_test(dwi2mask/b02template_antsfull_clioptions) add_bash_script_test(dwi2mask/b02template_antsfull_configfile) @@ -58,26 +63,37 @@ add_bash_script_test(dwi2mask/b02template_fsl_default) add_bash_script_test(dwi2mask/b02template_fsl_flirtconfig) add_bash_script_test(dwi2mask/b02template_fsl_fnirtcnf) add_bash_script_test(dwi2mask/b02template_fsl_fnirtconfig) -add_bash_script_test(dwi2mask/consensus) +add_bash_script_test(dwi2mask/b02template_piping) +add_bash_script_test(dwi2mask/consensus_default "pythonci") +add_bash_script_test(dwi2mask/consensus_piping) +add_bash_script_test(dwi2mask/consensus_template) add_bash_script_test(dwi2mask/fslbet_default) add_bash_script_test(dwi2mask/fslbet_options) +add_bash_script_test(dwi2mask/fslbet_piping) add_bash_script_test(dwi2mask/fslbet_rescale) -add_bash_script_test(dwi2mask/hdbet) -add_bash_script_test(dwi2mask/legacy "pythonci") -add_bash_script_test(dwi2mask/mean "pythonci") +add_bash_script_test(dwi2mask/hdbet_default) +add_bash_script_test(dwi2mask/hdbet_piping) +add_bash_script_test(dwi2mask/legacy_default "pythonci") +add_bash_script_test(dwi2mask/legacy_piping) +add_bash_script_test(dwi2mask/mean_default "pythonci") +add_bash_script_test(dwi2mask/mean_piping) add_bash_script_test(dwi2mask/mtnorm_default "pythonci") add_bash_script_test(dwi2mask/mtnorm_initmask) add_bash_script_test(dwi2mask/mtnorm_lmax) +add_bash_script_test(dwi2mask/mtnorm_piping) add_bash_script_test(dwi2mask/synthstrip_default) add_bash_script_test(dwi2mask/synthstrip_options) +add_bash_script_test(dwi2mask/synthstrip_piping) add_bash_script_test(dwi2mask/trace_default "pythonci") add_bash_script_test(dwi2mask/trace_iterative) +add_bash_script_test(dwi2mask/trace_piping) add_bash_script_test(dwi2response/dhollander_default) add_bash_script_test(dwi2response/dhollander_fslgrad "pythonci") add_bash_script_test(dwi2response/dhollander_grad) add_bash_script_test(dwi2response/dhollander_lmax) add_bash_script_test(dwi2response/dhollander_mask) +add_bash_script_test(dwi2response/dhollander_piping) add_bash_script_test(dwi2response/dhollander_shells) add_bash_script_test(dwi2response/dhollander_wmalgofa) add_bash_script_test(dwi2response/dhollander_wmalgotax) @@ -87,6 +103,7 @@ add_bash_script_test(dwi2response/fa_fslgrad "pythonci") add_bash_script_test(dwi2response/fa_grad) add_bash_script_test(dwi2response/fa_lmax) add_bash_script_test(dwi2response/fa_mask) +add_bash_script_test(dwi2response/fa_piping) add_bash_script_test(dwi2response/fa_shells) add_bash_script_test(dwi2response/fa_threshold) add_bash_script_test(dwi2response/manual_default) @@ -94,6 +111,7 @@ add_bash_script_test(dwi2response/manual_dirs) add_bash_script_test(dwi2response/manual_fslgrad "pythonci") add_bash_script_test(dwi2response/manual_grad) add_bash_script_test(dwi2response/manual_lmax) +add_bash_script_test(dwi2response/manual_piping) add_bash_script_test(dwi2response/manual_shells) add_bash_script_test(dwi2response/msmt5tt_default) add_bash_script_test(dwi2response/msmt5tt_dirs) @@ -101,6 +119,7 @@ add_bash_script_test(dwi2response/msmt5tt_fslgrad "pythonci") add_bash_script_test(dwi2response/msmt5tt_grad) add_bash_script_test(dwi2response/msmt5tt_lmax) add_bash_script_test(dwi2response/msmt5tt_mask) +add_bash_script_test(dwi2response/msmt5tt_piping) add_bash_script_test(dwi2response/msmt5tt_sfwmfa) add_bash_script_test(dwi2response/msmt5tt_shells) add_bash_script_test(dwi2response/msmt5tt_wmalgotax) @@ -109,25 +128,31 @@ add_bash_script_test(dwi2response/tax_fslgrad "pythonci") add_bash_script_test(dwi2response/tax_grad) add_bash_script_test(dwi2response/tax_lmax) add_bash_script_test(dwi2response/tax_mask) +add_bash_script_test(dwi2response/tax_piping) add_bash_script_test(dwi2response/tax_shell) add_bash_script_test(dwi2response/tournier_default) add_bash_script_test(dwi2response/tournier_fslgrad "pythonci") add_bash_script_test(dwi2response/tournier_grad) add_bash_script_test(dwi2response/tournier_lmax) add_bash_script_test(dwi2response/tournier_mask) +add_bash_script_test(dwi2response/tournier_piping) add_bash_script_test(dwi2response/tournier_shell) add_bash_script_test(dwibiascorrect/ants_default) add_bash_script_test(dwibiascorrect/ants_mask) +add_bash_script_test(dwibiascorrect/ants_piping) add_bash_script_test(dwibiascorrect/fsl_default) add_bash_script_test(dwibiascorrect/fsl_masked) +add_bash_script_test(dwibiascorrect/fsl_piping) add_bash_script_test(dwibiascorrect/mtnorm_default "pythonci") add_bash_script_test(dwibiascorrect/mtnorm_lmax) add_bash_script_test(dwibiascorrect/mtnorm_masked) +add_bash_script_test(dwibiascorrect/mtnorm_piping) add_bash_script_test(dwibiasnormmask/default "pythonci") add_bash_script_test(dwibiasnormmask/lmax) add_bash_script_test(dwibiasnormmask/maxiters) +add_bash_script_test(dwibiasnormmask/piping) add_bash_script_test(dwibiasnormmask/reference) add_bash_script_test(dwibiasnormmask/scaled) @@ -135,11 +160,13 @@ add_bash_script_test(dwicat/3dimage) add_bash_script_test(dwicat/default_ownbzero "pythonci") add_bash_script_test(dwicat/default_sharedbzero) add_bash_script_test(dwicat/nointensity) +add_bash_script_test(dwicat/piping) add_bash_script_test(dwicat/rigidbody) add_bash_script_test(dwifslpreproc/axis_padding) add_bash_script_test(dwifslpreproc/eddyqc) add_bash_script_test(dwifslpreproc/permuted_volumes) +add_bash_script_test(dwifslpreproc/piping) add_bash_script_test(dwifslpreproc/rpeall) add_bash_script_test(dwifslpreproc/rpeheader_none) add_bash_script_test(dwifslpreproc/rpeheader_onerevbzero) @@ -149,18 +176,23 @@ add_bash_script_test(dwifslpreproc/rpepair_alignseepi) add_bash_script_test(dwifslpreproc/rpepair_default) add_bash_script_test(dwigradcheck/default) +add_bash_script_test(dwigradcheck/piping) -add_bash_script_test(dwinormalise/group "pythonci") +add_bash_script_test(dwinormalise/group_default "pythonci") +add_bash_script_test(dwinormalise/group_piping) add_bash_script_test(dwinormalise/manual_default "pythonci") add_bash_script_test(dwinormalise/manual_percentile) +add_bash_script_test(dwinormalise/manual_piping) add_bash_script_test(dwinormalise/mtnorm_default "pythonci") add_bash_script_test(dwinormalise/mtnorm_lmax) add_bash_script_test(dwinormalise/mtnorm_masked) +add_bash_script_test(dwinormalise/mtnorm_piping) add_bash_script_test(dwinormalise/mtnorm_reference) add_bash_script_test(dwinormalise/mtnorm_scaled) add_bash_script_test(dwishellmath/default "pythonci") add_bash_script_test(dwishellmath/oneshell) +add_bash_script_test(dwishellmath/piping) add_bash_script_test(for_each/echo "pythonci") add_bash_script_test(for_each/exclude "pythonci") @@ -168,9 +200,15 @@ add_bash_script_test(for_each/parallel "pythonci") add_bash_script_test(labelsgmfirst/default) add_bash_script_test(labelsgmfirst/freesurfer) +add_bash_script_test(labelsgmfirst/piping) add_bash_script_test(labelsgmfirst/premasked) add_bash_script_test(labelsgmfirst/sgm_amyg_hipp) +add_bash_script_test(mask2glass/default "pythonci") + +add_bash_script_test(mrtrix_cleanup/default "pythonci") +add_bash_script_test(mrtrix_cleanup/test "pythonci") + add_bash_script_test(population_template/fa_affine) add_bash_script_test(population_template/fa_affinenonlinear) add_bash_script_test(population_template/fa_default "pythonci") @@ -184,6 +222,7 @@ add_bash_script_test(population_template/fa_rigidnonlinear) add_bash_script_test(population_template/fa_voxelsize) add_bash_script_test(population_template/fod_default "pythonci") add_bash_script_test(population_template/fod_options) +add_bash_script_test(population_template/piping) add_bash_script_test(responsemean/default "pythonci") add_bash_script_test(responsemean/legacy "pythonci") diff --git a/testing/scripts/tests/dwi2mask/3dautomask_piping b/testing/scripts/tests/dwi2mask/3dautomask_piping new file mode 100644 index 0000000000..cc0b540ae0 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/3dautomask_piping @@ -0,0 +1,7 @@ +#!/bin/bash +# Verify successful execution of "dwi2mask 3dautomask" +# when utilising image pipes +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwi2mask 3dautomask - - | \ +mrconvert - tmp.mif -force diff --git a/testing/scripts/tests/dwi2mask/ants_piping b/testing/scripts/tests/dwi2mask/ants_piping new file mode 100644 index 0000000000..b327d7868f --- /dev/null +++ b/testing/scripts/tests/dwi2mask/ants_piping @@ -0,0 +1,22 @@ +#!/bin/bash +# Verify that command "dwi2mask ants" executes to completion +# when utilising image pipes + +# Input and output images are pipes +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - | +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwi2mask ants - - \ +-template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz | \ +mrconvert - tmp.mif -force + +# Input template image is a pipe +mrconvert dwi2mask/template_image.mif.gz - | \ +dwi2mask ants BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-template - dwi2mask/template_mask.mif.gz + +# Input template mask is a pipe +mrconvert dwi2mask/template_mask.mif.gz - | \ +dwi2mask ants BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-template dwi2mask/template_image.mif.gz - diff --git a/testing/scripts/tests/dwi2mask/consensus b/testing/scripts/tests/dwi2mask/consensus deleted file mode 100644 index adc5071140..0000000000 --- a/testing/scripts/tests/dwi2mask/consensus +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# Verify execution of "dwi2mask consensus" algorithm -# Template image and corresponding mask are provided -# to maximise the cardinality of applicable algorithms; -# there is however no verification that all underlying invoked algorithms -# proceed to completion and contribute to the consensus -dwi2mask consensus BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp1.mif -force \ --fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --masks tmp2.mif \ --template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz diff --git a/testing/scripts/tests/dwi2mask/consensus_default b/testing/scripts/tests/dwi2mask/consensus_default new file mode 100644 index 0000000000..017b9f0af9 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/consensus_default @@ -0,0 +1,9 @@ +#!/bin/bash +# Verify execution of "dwi2mask consensus" algorithm +# under default operation +# Here the -template option is omitted, +# resulting in the minimal number of algorithms utilized + +dwi2mask consensus BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-masks tmp_masks.mif diff --git a/testing/scripts/tests/dwi2mask/consensus_piping b/testing/scripts/tests/dwi2mask/consensus_piping new file mode 100644 index 0000000000..4b2d237355 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/consensus_piping @@ -0,0 +1,27 @@ +#!/bin/bash +# Verify execution of "dwi2mask consensus" algorithm +# when utilising image pipes + +# Input and output images are pipes +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwi2mask consensus - - | \ +mrconvert - tmp.mif -force + +# Input template image is a pipe +mrconvert dwi2mask/template_image.mif.gz - | \ +dwi2mask consensus BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-template - dwi2mask/template_mask.mif.gz + +# Input template mask is a pipe +mrconvert dwi2mask/template_mask.mif.gz - | \ +dwi2mask consensus BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-template dwi2mask/template_image.mif.gz - + +# Output mask series is a pipe +dwi2mask consensus BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-masks - | \ +mrconvert - tmp.mif -force diff --git a/testing/scripts/tests/dwi2mask/consensus_template b/testing/scripts/tests/dwi2mask/consensus_template new file mode 100644 index 0000000000..62b26ab24f --- /dev/null +++ b/testing/scripts/tests/dwi2mask/consensus_template @@ -0,0 +1,11 @@ +#!/bin/bash +# Verify execution of "dwi2mask consensus" algorithm +# under default operation +# Here the -template option is provided, +# resulting in a greater number of algorithms utilized +# than the default test + +dwi2mask consensus BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-masks tmp_masks.mif \ +-template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz diff --git a/testing/scripts/tests/dwi2mask/fslbet_piping b/testing/scripts/tests/dwi2mask/fslbet_piping new file mode 100644 index 0000000000..35759efa0c --- /dev/null +++ b/testing/scripts/tests/dwi2mask/fslbet_piping @@ -0,0 +1,7 @@ +#!/bin/bash +# Ensure successful execution of "dwi2mask fslbet" algorithm +# when making use of image pipes +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwi2mask fslbet - - | \ +mrconvert - tmp.mif -force diff --git a/testing/scripts/tests/dwi2mask/hdbet b/testing/scripts/tests/dwi2mask/hdbet_default similarity index 88% rename from testing/scripts/tests/dwi2mask/hdbet rename to testing/scripts/tests/dwi2mask/hdbet_default index 5e87c33f7a..6cc0ad3002 100644 --- a/testing/scripts/tests/dwi2mask/hdbet +++ b/testing/scripts/tests/dwi2mask/hdbet_default @@ -1,4 +1,5 @@ #!/bin/bash # Verify successful execution of "dwi2mask hdbet" algorithm +# under default operation dwi2mask hdbet BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval diff --git a/testing/scripts/tests/dwi2mask/hdbet_piping b/testing/scripts/tests/dwi2mask/hdbet_piping new file mode 100644 index 0000000000..c4a30b718a --- /dev/null +++ b/testing/scripts/tests/dwi2mask/hdbet_piping @@ -0,0 +1,7 @@ +#!/bin/bash +# Verify successful execution of "dwi2mask hdbet" algorithm +# when using image piping +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwi2mask hdbet - - | \ +mrconvert - tmp.mif -force diff --git a/testing/scripts/tests/dwi2mask/mean b/testing/scripts/tests/dwi2mask/mean_default similarity index 89% rename from testing/scripts/tests/dwi2mask/mean rename to testing/scripts/tests/dwi2mask/mean_default index fbf0ec01bc..d910f87e3e 100644 --- a/testing/scripts/tests/dwi2mask/mean +++ b/testing/scripts/tests/dwi2mask/mean_default @@ -1,5 +1,6 @@ #!/bin/bash # Verify "dwi2mask mean" algorithm +# under default operation dwi2mask mean BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval diff --git a/testing/scripts/tests/dwi2mask/mean_piping b/testing/scripts/tests/dwi2mask/mean_piping new file mode 100644 index 0000000000..fcd946b1c8 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/mean_piping @@ -0,0 +1,7 @@ +#!/bin/bash +# Verify "dwi2mask mean" algorithm +# when used in conjunction with image pipes +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwi2mask mean - - | \ +testing_diff_image - dwi2mask/mean.mif.gz diff --git a/testing/scripts/tests/dwi2mask/mtnorm_initmask b/testing/scripts/tests/dwi2mask/mtnorm_initmask index 7955c50466..faec22dc28 100644 --- a/testing/scripts/tests/dwi2mask/mtnorm_initmask +++ b/testing/scripts/tests/dwi2mask/mtnorm_initmask @@ -1,7 +1,7 @@ #!/bin/bash # Verify "dwi2mask mtnorm" algorithm, # where an initial brain mask estimate is provided by the user -dwi2mask mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmmpmask.mif -force \ +dwi2mask mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmpmask.mif -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ -init_mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz \ -tissuesum tmptissuesum.mif diff --git a/testing/scripts/tests/dwi2mask/trace_piping b/testing/scripts/tests/dwi2mask/trace_piping new file mode 100644 index 0000000000..c02cfc38e0 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/trace_piping @@ -0,0 +1,7 @@ +#!/bin/bash +# Verify result of "dwi2mask trace" algorithm +# when utilising image pipes +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - | +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwi2mask trace - - | \ +testing_diff_image - dwi2mask/trace_default.mif.gz diff --git a/testing/scripts/tests/dwibiasnormmask/piping b/testing/scripts/tests/dwibiasnormmask/piping new file mode 100644 index 0000000000..b6680178c3 --- /dev/null +++ b/testing/scripts/tests/dwibiasnormmask/piping @@ -0,0 +1,28 @@ +#!/bin/bash +# Verify successful execution of command +# when used in conjunction with image pipes + +# Input DWI series, and output DWI series, are pipes +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ +dwibiasnormmask - - | \ +testing_diff_image - dwibiasnormmask/default_out.mif.gz -frac 1e-5 + +# Prepare some input data to be subsequently used in multiple tests +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_in.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-strides 0,0,0,1 + +# Output mask is a pipe +dwibiasnormmask tmp_in.mif tmp_out.mif - -force | \ +testing_diff_image - dwibiasnormmask/default_mask.mif.gz + +# Output bias field image is a pipe +dwibiasnormmask tmp_in.mif tmp_out.mif tmp_mask.mif -force \ +-output_bias - | \ +testing_diff_image tmpbias.mif dwibiasnormmask/default_bias.mif.gz -frac 1e-5 + +# Output tissue sum image is a pipe +dwibiasnormmask tmp_in.mif tmp_out.mif tmp_mask.mif -force \ +-output_tissuesum - | \ +testing_diff_image tmptissuesum.mif dwibiasnormmask/default_tissuesum.mif.gz -abs 1e-5 diff --git a/testing/scripts/tests/labelsgmfirst/piping b/testing/scripts/tests/labelsgmfirst/piping new file mode 100644 index 0000000000..eed573ace4 --- /dev/null +++ b/testing/scripts/tests/labelsgmfirst/piping @@ -0,0 +1,13 @@ +#!/bin/bash +# Verify operation of command +# when utilising image pipes + +# Input and output parcellation images are pipes +mrconvert BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz - | \ +labelsgmfirst - BIDS/sub-01/anat/sub-01_T1w.nii.gz BIDS/parc-desikan_lookup.txt - | \ +testing_diff_header - labelsgmfirst/default.mif.gz + +# Input T1-weighted image is a pipe +mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz - | +labelsgmfirst BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz - BIDS/parc-desikan_lookup.txt tmp.mif -force +testig_diff_header tmp.mif labelsgmfirst/default.mif.gz diff --git a/testing/scripts/tests/mask2glass/default b/testing/scripts/tests/mask2glass/default new file mode 100644 index 0000000000..a2abd6e71d --- /dev/null +++ b/testing/scripts/tests/mask2glass/default @@ -0,0 +1,5 @@ +#!/bin/bash +# Verify operation of the "mask2glass" command +# under default operation +mask2glass BIDS/sub-01/dwi/sub-01_brainmask.nii.gz tmp.mif -force +testing_diff_image tmp.mif mask2glass/out.mif.gz diff --git a/testing/scripts/tests/mask2glass/piping b/testing/scripts/tests/mask2glass/piping new file mode 100644 index 0000000000..6450dd0970 --- /dev/null +++ b/testing/scripts/tests/mask2glass/piping @@ -0,0 +1,6 @@ +#!/bin/bash +# Verify operation of the "mask2glass" command +# where image piping is used +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | \ +mask2glass - - | \ +testing_diff_image - mask2glass/out.mif.gz diff --git a/testing/scripts/tests/mrtrix_cleanup/default b/testing/scripts/tests/mrtrix_cleanup/default new file mode 100644 index 0000000000..bf5da49eab --- /dev/null +++ b/testing/scripts/tests/mrtrix_cleanup/default @@ -0,0 +1,16 @@ +#!/bin/bash +# Verify basic operation of the command +# when cleanup is activated +rm -rf tmp/ +mkdir tmp/ + +touch tmp/not_a_tempfile.txt +touch tmp/mrtrix-tmp-ABCDEF.mif +mkdir tmp/not_a_scratchdir/ +mkdir tmp/command-tmp-ABCDEF/ + +mrtrix_cleanup tmp/ +ITEMCOUNT=$(ls tmp/ | wc) +if [ "$ITEMCOUNT" -neq "2" ]; then + exit 1 +fi diff --git a/testing/scripts/tests/mrtrix_cleanup/test b/testing/scripts/tests/mrtrix_cleanup/test new file mode 100644 index 0000000000..75ddcfc536 --- /dev/null +++ b/testing/scripts/tests/mrtrix_cleanup/test @@ -0,0 +1,15 @@ +#!/bin/bash +# Verify basic operation of the command +rm -rf tmp/ +mkdir tmp/ + +touch tmp/not_a_tempfile.txt +touch tmp/mrtrix-tmp-ABCDEF.mif +mkdir tmp/not_a_scratchdir/ +mkdir tmp/command-tmp-ABCDEF/ + +mrtrix_cleanup tmp/ -test +ITEMCOUNT=$(ls tmp/ | wc) +if [ "$ITEMCOUNT" -neq "4" ]; then + exit 1 +fi From 99dd92741cf156a094902faac3f990af78b2106e Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 13 May 2024 21:29:22 +1000 Subject: [PATCH 63/75] mask2glass: Ensure input image is 3D --- python/bin/mask2glass | 5 +++++ testing/scripts/CMakeLists.txt | 1 + testing/scripts/tests/mask2glass/no4dseries | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 testing/scripts/tests/mask2glass/no4dseries diff --git a/python/bin/mask2glass b/python/bin/mask2glass index bff040d35a..80c88b3e3a 100755 --- a/python/bin/mask2glass +++ b/python/bin/mask2glass @@ -54,8 +54,13 @@ def usage(cmdline): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable + from mrtrix3 import MRtrixError #pylint: disable=no-name-in-module, import-outside-toplevel from mrtrix3 import app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel + image_size = image.Header(app.ARGS.input).size() + if not len(image_size) == 3 or (len(image_size == 4) and image_size[-1] == 1): + raise MRtrixError('Command expects as input a 3D image') + # import data to scratch directory app.activate_scratch_dir() run.command(['mrconvert', app.ARGS.input, 'in.mif'], diff --git a/testing/scripts/CMakeLists.txt b/testing/scripts/CMakeLists.txt index a32ebbdcde..0877545d13 100644 --- a/testing/scripts/CMakeLists.txt +++ b/testing/scripts/CMakeLists.txt @@ -205,6 +205,7 @@ add_bash_script_test(labelsgmfirst/premasked) add_bash_script_test(labelsgmfirst/sgm_amyg_hipp) add_bash_script_test(mask2glass/default "pythonci") +add_bash_script_test(mask2glass/no4dseries "pythonci") add_bash_script_test(mrtrix_cleanup/default "pythonci") add_bash_script_test(mrtrix_cleanup/test "pythonci") diff --git a/testing/scripts/tests/mask2glass/no4dseries b/testing/scripts/tests/mask2glass/no4dseries new file mode 100644 index 0000000000..4cf239f297 --- /dev/null +++ b/testing/scripts/tests/mask2glass/no4dseries @@ -0,0 +1,6 @@ +#!/bin/bash +# Verify that if the "mask2glass" command is provided +# with a 4D image series as input, +# it exits gracefully with an appropriate error message +mask2glass BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force 2>&1 | \ +grep "Command expects as input a 3D image" From 7115593deac26a7303d41f49dbce9c595f62af94 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 13 May 2024 23:28:32 +1000 Subject: [PATCH 64/75] Testing: Verify that Python commands handle paths with whitespaces --- testing/scripts/CMakeLists.txt | 37 +++++++++++++++++++ .../tests/5ttgen/freesurfer_whitespace | 6 +++ testing/scripts/tests/5ttgen/fsl_whitespace | 11 ++++++ testing/scripts/tests/5ttgen/hsvs_whitespace | 6 +++ .../tests/dwi2mask/3dautomask_whitespace | 14 +++++++ .../scripts/tests/dwi2mask/ants_whitespace | 18 +++++++++ .../tests/dwi2mask/b02template_whitespace | 19 ++++++++++ .../tests/dwi2mask/consensus_whitespace | 19 ++++++++++ .../scripts/tests/dwi2mask/fslbet_whitespace | 14 +++++++ .../scripts/tests/dwi2mask/hdbet_whitespace | 14 +++++++ .../scripts/tests/dwi2mask/legacy_whitespace | 11 ++++++ .../scripts/tests/dwi2mask/mean_whitespace | 11 ++++++ .../scripts/tests/dwi2mask/mtnorm_whitespace | 14 +++++++ .../tests/dwi2mask/synthstrip_whitespace | 16 ++++++++ .../scripts/tests/dwi2mask/trace_whitespace | 11 ++++++ .../tests/dwi2response/dhollander_whitespace | 17 +++++++++ .../scripts/tests/dwi2response/fa_whitespace | 16 ++++++++ .../tests/dwi2response/manual_whitespace | 20 ++++++++++ .../tests/dwi2response/msmt5tt_whitespace | 26 +++++++++++++ .../scripts/tests/dwi2response/tax_whitespace | 15 ++++++++ .../tests/dwi2response/tournier_whitespace | 17 +++++++++ .../tests/dwibiascorrect/ants_whitespace | 16 ++++++++ .../tests/dwibiascorrect/fsl_whitespace | 17 +++++++++ .../tests/dwibiascorrect/mtnorm_whitespace | 16 ++++++++ testing/scripts/tests/dwibiasnormmask/piping | 5 +++ .../scripts/tests/dwibiasnormmask/whitespace | 21 +++++++++++ testing/scripts/tests/dwicat/whitespace | 15 ++++++++ .../scripts/tests/dwifslpreproc/whitespace | 32 ++++++++++++++++ testing/scripts/tests/dwigradcheck/whitespace | 18 +++++++++ .../tests/dwinormalise/group_whitespace | 21 +++++++++++ .../tests/dwinormalise/manual_whitespace | 14 +++++++ .../tests/dwinormalise/mtnorm_whitespace | 14 +++++++ testing/scripts/tests/dwishellmath/whitespace | 12 ++++++ .../scripts/tests/labelsgmfirst/whitespace | 11 ++++++ testing/scripts/tests/mask2glass/whitespace | 8 ++++ .../scripts/tests/mrtrix_cleanup/whitespace | 16 ++++++++ .../tests/population_template/whitespace | 31 ++++++++++++++++ testing/scripts/tests/responsemean/whitespace | 7 ++++ 38 files changed, 606 insertions(+) create mode 100644 testing/scripts/tests/5ttgen/freesurfer_whitespace create mode 100644 testing/scripts/tests/5ttgen/fsl_whitespace create mode 100644 testing/scripts/tests/5ttgen/hsvs_whitespace create mode 100644 testing/scripts/tests/dwi2mask/3dautomask_whitespace create mode 100644 testing/scripts/tests/dwi2mask/ants_whitespace create mode 100644 testing/scripts/tests/dwi2mask/b02template_whitespace create mode 100644 testing/scripts/tests/dwi2mask/consensus_whitespace create mode 100644 testing/scripts/tests/dwi2mask/fslbet_whitespace create mode 100644 testing/scripts/tests/dwi2mask/hdbet_whitespace create mode 100644 testing/scripts/tests/dwi2mask/legacy_whitespace create mode 100644 testing/scripts/tests/dwi2mask/mean_whitespace create mode 100644 testing/scripts/tests/dwi2mask/mtnorm_whitespace create mode 100644 testing/scripts/tests/dwi2mask/synthstrip_whitespace create mode 100644 testing/scripts/tests/dwi2mask/trace_whitespace create mode 100644 testing/scripts/tests/dwi2response/dhollander_whitespace create mode 100644 testing/scripts/tests/dwi2response/fa_whitespace create mode 100644 testing/scripts/tests/dwi2response/manual_whitespace create mode 100644 testing/scripts/tests/dwi2response/msmt5tt_whitespace create mode 100644 testing/scripts/tests/dwi2response/tax_whitespace create mode 100644 testing/scripts/tests/dwi2response/tournier_whitespace create mode 100644 testing/scripts/tests/dwibiascorrect/ants_whitespace create mode 100644 testing/scripts/tests/dwibiascorrect/fsl_whitespace create mode 100644 testing/scripts/tests/dwibiascorrect/mtnorm_whitespace create mode 100644 testing/scripts/tests/dwibiasnormmask/whitespace create mode 100644 testing/scripts/tests/dwicat/whitespace create mode 100644 testing/scripts/tests/dwifslpreproc/whitespace create mode 100644 testing/scripts/tests/dwigradcheck/whitespace create mode 100644 testing/scripts/tests/dwinormalise/group_whitespace create mode 100644 testing/scripts/tests/dwinormalise/manual_whitespace create mode 100644 testing/scripts/tests/dwinormalise/mtnorm_whitespace create mode 100644 testing/scripts/tests/dwishellmath/whitespace create mode 100644 testing/scripts/tests/labelsgmfirst/whitespace create mode 100644 testing/scripts/tests/mask2glass/whitespace create mode 100644 testing/scripts/tests/mrtrix_cleanup/whitespace create mode 100644 testing/scripts/tests/population_template/whitespace create mode 100644 testing/scripts/tests/responsemean/whitespace diff --git a/testing/scripts/CMakeLists.txt b/testing/scripts/CMakeLists.txt index 0877545d13..5ad1f0c9af 100644 --- a/testing/scripts/CMakeLists.txt +++ b/testing/scripts/CMakeLists.txt @@ -33,26 +33,31 @@ add_bash_script_test(5ttgen/freesurfer_default "pythonci") add_bash_script_test(5ttgen/freesurfer_nocrop) add_bash_script_test(5ttgen/freesurfer_piping) add_bash_script_test(5ttgen/freesurfer_sgmamyghipp) +add_bash_script_test(5ttgen/freesurfer_whitespace) add_bash_script_test(5ttgen/fsl_default) add_bash_script_test(5ttgen/fsl_mask) add_bash_script_test(5ttgen/fsl_nocrop) add_bash_script_test(5ttgen/fsl_piping) add_bash_script_test(5ttgen/fsl_premasked) add_bash_script_test(5ttgen/fsl_sgmamyghipp) +add_bash_script_test(5ttgen/fsl_whitespace) add_bash_script_test(5ttgen/hsvs_aseg) add_bash_script_test(5ttgen/hsvs_default) add_bash_script_test(5ttgen/hsvs_first) add_bash_script_test(5ttgen/hsvs_modules) add_bash_script_test(5ttgen/hsvs_piping) add_bash_script_test(5ttgen/hsvs_template) +add_bash_script_test(5ttgen/hsvs_whitespace) add_bash_script_test(5ttgen/hsvs_whitestem) add_bash_script_test(dwi2mask/3dautomask_default) add_bash_script_test(dwi2mask/3dautomask_options) add_bash_script_test(dwi2mask/3dautomask_piping) +add_bash_script_test(dwi2mask/3dautomask_whitespace) add_bash_script_test(dwi2mask/ants_config) add_bash_script_test(dwi2mask/ants_piping) add_bash_script_test(dwi2mask/ants_template) +add_bash_script_test(dwi2mask/ants_whitespace) add_bash_script_test(dwi2mask/b02template_antsfull_clioptions) add_bash_script_test(dwi2mask/b02template_antsfull_configfile) add_bash_script_test(dwi2mask/b02template_antsfull_configoptions) @@ -64,29 +69,38 @@ add_bash_script_test(dwi2mask/b02template_fsl_flirtconfig) add_bash_script_test(dwi2mask/b02template_fsl_fnirtcnf) add_bash_script_test(dwi2mask/b02template_fsl_fnirtconfig) add_bash_script_test(dwi2mask/b02template_piping) +add_bash_script_test(dwi2mask/b02template_whitespace) add_bash_script_test(dwi2mask/consensus_default "pythonci") add_bash_script_test(dwi2mask/consensus_piping) add_bash_script_test(dwi2mask/consensus_template) +add_bash_script_test(dwi2mask/consensus_whitespace) add_bash_script_test(dwi2mask/fslbet_default) add_bash_script_test(dwi2mask/fslbet_options) add_bash_script_test(dwi2mask/fslbet_piping) add_bash_script_test(dwi2mask/fslbet_rescale) +add_bash_script_test(dwi2mask/fslbet_whitespace) add_bash_script_test(dwi2mask/hdbet_default) add_bash_script_test(dwi2mask/hdbet_piping) +add_bash_script_test(dwi2mask/hdbet_whitespace) add_bash_script_test(dwi2mask/legacy_default "pythonci") add_bash_script_test(dwi2mask/legacy_piping) +add_bash_script_test(dwi2mask/legacy_whitespace) add_bash_script_test(dwi2mask/mean_default "pythonci") add_bash_script_test(dwi2mask/mean_piping) +add_bash_script_test(dwi2mask/mean_whitespace) add_bash_script_test(dwi2mask/mtnorm_default "pythonci") add_bash_script_test(dwi2mask/mtnorm_initmask) add_bash_script_test(dwi2mask/mtnorm_lmax) add_bash_script_test(dwi2mask/mtnorm_piping) +add_bash_script_test(dwi2mask/mtnorm_whitespace) add_bash_script_test(dwi2mask/synthstrip_default) add_bash_script_test(dwi2mask/synthstrip_options) add_bash_script_test(dwi2mask/synthstrip_piping) +add_bash_script_test(dwi2mask/synthstrip_whitespace) add_bash_script_test(dwi2mask/trace_default "pythonci") add_bash_script_test(dwi2mask/trace_iterative) add_bash_script_test(dwi2mask/trace_piping) +add_bash_script_test(dwi2mask/trace_whitespace) add_bash_script_test(dwi2response/dhollander_default) add_bash_script_test(dwi2response/dhollander_fslgrad "pythonci") @@ -95,6 +109,7 @@ add_bash_script_test(dwi2response/dhollander_lmax) add_bash_script_test(dwi2response/dhollander_mask) add_bash_script_test(dwi2response/dhollander_piping) add_bash_script_test(dwi2response/dhollander_shells) +add_bash_script_test(dwi2response/dhollander_whitespace) add_bash_script_test(dwi2response/dhollander_wmalgofa) add_bash_script_test(dwi2response/dhollander_wmalgotax) add_bash_script_test(dwi2response/dhollander_wmalgotournier) @@ -106,6 +121,7 @@ add_bash_script_test(dwi2response/fa_mask) add_bash_script_test(dwi2response/fa_piping) add_bash_script_test(dwi2response/fa_shells) add_bash_script_test(dwi2response/fa_threshold) +add_bash_script_test(dwi2response/fa_whitespace) add_bash_script_test(dwi2response/manual_default) add_bash_script_test(dwi2response/manual_dirs) add_bash_script_test(dwi2response/manual_fslgrad "pythonci") @@ -113,6 +129,7 @@ add_bash_script_test(dwi2response/manual_grad) add_bash_script_test(dwi2response/manual_lmax) add_bash_script_test(dwi2response/manual_piping) add_bash_script_test(dwi2response/manual_shells) +add_bash_script_test(dwi2response/manual_whitespace) add_bash_script_test(dwi2response/msmt5tt_default) add_bash_script_test(dwi2response/msmt5tt_dirs) add_bash_script_test(dwi2response/msmt5tt_fslgrad "pythonci") @@ -122,6 +139,7 @@ add_bash_script_test(dwi2response/msmt5tt_mask) add_bash_script_test(dwi2response/msmt5tt_piping) add_bash_script_test(dwi2response/msmt5tt_sfwmfa) add_bash_script_test(dwi2response/msmt5tt_shells) +add_bash_script_test(dwi2response/msmt5tt_whitespace) add_bash_script_test(dwi2response/msmt5tt_wmalgotax) add_bash_script_test(dwi2response/tax_default) add_bash_script_test(dwi2response/tax_fslgrad "pythonci") @@ -130,6 +148,7 @@ add_bash_script_test(dwi2response/tax_lmax) add_bash_script_test(dwi2response/tax_mask) add_bash_script_test(dwi2response/tax_piping) add_bash_script_test(dwi2response/tax_shell) +add_bash_script_test(dwi2response/tax_whitespace) add_bash_script_test(dwi2response/tournier_default) add_bash_script_test(dwi2response/tournier_fslgrad "pythonci") add_bash_script_test(dwi2response/tournier_grad) @@ -137,17 +156,21 @@ add_bash_script_test(dwi2response/tournier_lmax) add_bash_script_test(dwi2response/tournier_mask) add_bash_script_test(dwi2response/tournier_piping) add_bash_script_test(dwi2response/tournier_shell) +add_bash_script_test(dwi2response/tournier_whitespace) add_bash_script_test(dwibiascorrect/ants_default) add_bash_script_test(dwibiascorrect/ants_mask) add_bash_script_test(dwibiascorrect/ants_piping) +add_bash_script_test(dwibiascorrect/ants_whitespace) add_bash_script_test(dwibiascorrect/fsl_default) add_bash_script_test(dwibiascorrect/fsl_masked) add_bash_script_test(dwibiascorrect/fsl_piping) +add_bash_script_test(dwibiascorrect/fsl_whitespace) add_bash_script_test(dwibiascorrect/mtnorm_default "pythonci") add_bash_script_test(dwibiascorrect/mtnorm_lmax) add_bash_script_test(dwibiascorrect/mtnorm_masked) add_bash_script_test(dwibiascorrect/mtnorm_piping) +add_bash_script_test(dwibiascorrect/mtnorm_whitespace) add_bash_script_test(dwibiasnormmask/default "pythonci") add_bash_script_test(dwibiasnormmask/lmax) @@ -155,6 +178,7 @@ add_bash_script_test(dwibiasnormmask/maxiters) add_bash_script_test(dwibiasnormmask/piping) add_bash_script_test(dwibiasnormmask/reference) add_bash_script_test(dwibiasnormmask/scaled) +add_bash_script_test(dwibiasnormmask/whitespace) add_bash_script_test(dwicat/3dimage) add_bash_script_test(dwicat/default_ownbzero "pythonci") @@ -162,6 +186,7 @@ add_bash_script_test(dwicat/default_sharedbzero) add_bash_script_test(dwicat/nointensity) add_bash_script_test(dwicat/piping) add_bash_script_test(dwicat/rigidbody) +add_bash_script_test(dwicat/whitespace) add_bash_script_test(dwifslpreproc/axis_padding) add_bash_script_test(dwifslpreproc/eddyqc) @@ -174,25 +199,31 @@ add_bash_script_test(dwifslpreproc/rpeheader_rpepair) add_bash_script_test(dwifslpreproc/rpenone_default) add_bash_script_test(dwifslpreproc/rpepair_alignseepi) add_bash_script_test(dwifslpreproc/rpepair_default) +add_bash_script_test(dwifslpreproc/whitespace) add_bash_script_test(dwigradcheck/default) add_bash_script_test(dwigradcheck/piping) +add_bash_script_test(dwigradcheck/whitespace) add_bash_script_test(dwinormalise/group_default "pythonci") add_bash_script_test(dwinormalise/group_piping) +add_bash_script_test(dwinormalise/group_whitespace) add_bash_script_test(dwinormalise/manual_default "pythonci") add_bash_script_test(dwinormalise/manual_percentile) add_bash_script_test(dwinormalise/manual_piping) +add_bash_script_test(dwinormalise/manual_whitespace) add_bash_script_test(dwinormalise/mtnorm_default "pythonci") add_bash_script_test(dwinormalise/mtnorm_lmax) add_bash_script_test(dwinormalise/mtnorm_masked) add_bash_script_test(dwinormalise/mtnorm_piping) add_bash_script_test(dwinormalise/mtnorm_reference) add_bash_script_test(dwinormalise/mtnorm_scaled) +add_bash_script_test(dwinormalise/mtnorm_whitespace) add_bash_script_test(dwishellmath/default "pythonci") add_bash_script_test(dwishellmath/oneshell) add_bash_script_test(dwishellmath/piping) +add_bash_script_test(dwishellmath/whitespace) add_bash_script_test(for_each/echo "pythonci") add_bash_script_test(for_each/exclude "pythonci") @@ -203,12 +234,16 @@ add_bash_script_test(labelsgmfirst/freesurfer) add_bash_script_test(labelsgmfirst/piping) add_bash_script_test(labelsgmfirst/premasked) add_bash_script_test(labelsgmfirst/sgm_amyg_hipp) +add_bash_script_test(labelsgmfirst/whitespace) add_bash_script_test(mask2glass/default "pythonci") add_bash_script_test(mask2glass/no4dseries "pythonci") +add_bash_script_test(mask2glass/piping) +add_bash_script_test(mask2glass/whitespace) add_bash_script_test(mrtrix_cleanup/default "pythonci") add_bash_script_test(mrtrix_cleanup/test "pythonci") +add_bash_script_test(mrtrix_cleanup/whitespace) add_bash_script_test(population_template/fa_affine) add_bash_script_test(population_template/fa_affinenonlinear) @@ -224,7 +259,9 @@ add_bash_script_test(population_template/fa_voxelsize) add_bash_script_test(population_template/fod_default "pythonci") add_bash_script_test(population_template/fod_options) add_bash_script_test(population_template/piping) +add_bash_script_test(population_template/whitespace) add_bash_script_test(responsemean/default "pythonci") add_bash_script_test(responsemean/legacy "pythonci") +add_bash_script_test(responsemean/whitespace "pythonci") diff --git a/testing/scripts/tests/5ttgen/freesurfer_whitespace b/testing/scripts/tests/5ttgen/freesurfer_whitespace new file mode 100644 index 0000000000..1610756b6a --- /dev/null +++ b/testing/scripts/tests/5ttgen/freesurfer_whitespace @@ -0,0 +1,6 @@ +#!/bin/bash +# Ensure correct operation of the "5ttgen freesurfer" command +# where image paths include whitespace +mrconvert BIDS/sub-01/anat/aparc+aseg.mgz "tmp in.mif" -force +5ttgen freesurfer "tmp in.mif" "tmp out.mif" -force +testing_diff_image "tmp out.mif" 5ttgen/freesurfer/default.mif.gz diff --git a/testing/scripts/tests/5ttgen/fsl_whitespace b/testing/scripts/tests/5ttgen/fsl_whitespace new file mode 100644 index 0000000000..64db02c8b4 --- /dev/null +++ b/testing/scripts/tests/5ttgen/fsl_whitespace @@ -0,0 +1,11 @@ +#!/bin/bash +# Ensure correct operation of the "5ttgen fsl" command +# where image paths include whitespace +# Make sure that the output image is stored at the expected path +rm -f "tmp out.mif" + +mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz "tmp in.mif" -force + +5ttgen fsl "tmp in.mif" "tmp out.mif" -force + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/5ttgen/hsvs_whitespace b/testing/scripts/tests/5ttgen/hsvs_whitespace new file mode 100644 index 0000000000..e92274a9b9 --- /dev/null +++ b/testing/scripts/tests/5ttgen/hsvs_whitespace @@ -0,0 +1,6 @@ +#!/bin/bash +# Ensure correct operation of the "5ttgen hsvs" command +# when filesystem paths include whitespace characters +ln -s "tmp in/" freesurfer/sub-01 +5ttgen hsvs "tmp in/" "tmp out.mif" -force +testing_diff_header "tmp out.mif" 5ttgen/hsvs/default.mif.gz diff --git a/testing/scripts/tests/dwi2mask/3dautomask_whitespace b/testing/scripts/tests/dwi2mask/3dautomask_whitespace new file mode 100644 index 0000000000..21e7fcfc70 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/3dautomask_whitespace @@ -0,0 +1,14 @@ +#!/bin/bash +# Verify successful execution of "dwi2mask 3dautomask" +# where image paths include whitespace characters +# Make sure that the output image appears at the expected location +rm -f "tmp in.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask 3dautomask "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/ants_whitespace b/testing/scripts/tests/dwi2mask/ants_whitespace new file mode 100644 index 0000000000..2259c3b321 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/ants_whitespace @@ -0,0 +1,18 @@ +#!/bin/bash +# Verify that command "dwi2mask ants" executes to completion +# when utilising image paths that include whitespace characters +# Make sure that the output image appears at the expected location +rm -f "tmp out.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" +ln -s dwi2mask/template_mask.mif.gz "tmp mask.mif.gz" + +dwi2mask ants "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-template "tmp template.mif.gz" "tmp mask.mif.gz" + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/b02template_whitespace b/testing/scripts/tests/dwi2mask/b02template_whitespace new file mode 100644 index 0000000000..9d29ced452 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/b02template_whitespace @@ -0,0 +1,19 @@ +#!/bin/bash +# Verify operation of the "dwi2mask b02template" command +# when image paths include whitespace characters +# Ensure that the output image appears at the expected location +rm -f "tmp out.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" +ln -s dwi2mask/template_mask.mif.hz "tmp mask.mif.gz" + +dwi2mask b02template "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-software antsquick \ +-template "tmp template.mif.gz" "tmp mask.mif.gz" + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/consensus_whitespace b/testing/scripts/tests/dwi2mask/consensus_whitespace new file mode 100644 index 0000000000..43e1e35dbd --- /dev/null +++ b/testing/scripts/tests/dwi2mask/consensus_whitespace @@ -0,0 +1,19 @@ +#!/bin/bash +# Verify execution of "dwi2mask consensus" algorithm +# when utilising image paths that include whitespace characters +# Make sure that output image appears at the expected location +rm -f "tmp out.mif" + +ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" +ln -s dwi2mask/template_mask.mif.gz "tmp mask.mif.gz" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask consensus "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-template "tmp template.mif.gz" "tmp mask.mif.gz" \ +-masks "tmp masks.mif" + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/fslbet_whitespace b/testing/scripts/tests/dwi2mask/fslbet_whitespace new file mode 100644 index 0000000000..1ce901ed3b --- /dev/null +++ b/testing/scripts/tests/dwi2mask/fslbet_whitespace @@ -0,0 +1,14 @@ +#!/bin/bash +# Ensure successful execution of "dwi2mask fslbet" algorithm +# when making use of image paths that include whitespace characters +# Make sure that output image appears at expected location +rm -f "tmp out.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask fslbet "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/hdbet_whitespace b/testing/scripts/tests/dwi2mask/hdbet_whitespace new file mode 100644 index 0000000000..bae7c94c88 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/hdbet_whitespace @@ -0,0 +1,14 @@ +#!/bin/bash +# Verify successful execution of "dwi2mask hdbet" algorithm +# when using image paths that include whitespace characters +# Make sure that output image appears at expected location +rm -f "tmp out.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask hdbet "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/legacy_whitespace b/testing/scripts/tests/dwi2mask/legacy_whitespace new file mode 100644 index 0000000000..4e544d30e4 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/legacy_whitespace @@ -0,0 +1,11 @@ +#!/bin/bash +# Ensure correct operation of the "dwi2mask legacy" command +# when image paths that include whitespace characters are used +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask legacy "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +testing_diff_image "tmp out.mif" dwi2mask/legacy.mif.gz diff --git a/testing/scripts/tests/dwi2mask/mean_whitespace b/testing/scripts/tests/dwi2mask/mean_whitespace new file mode 100644 index 0000000000..5fb5651816 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/mean_whitespace @@ -0,0 +1,11 @@ +#!/bin/bash +# Verify "dwi2mask mean" algorithm +# when image paths include whitespace characters +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask mean "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +testing_diff_image "tmp out.mif" dwi2mask/mean.mif.gz diff --git a/testing/scripts/tests/dwi2mask/mtnorm_whitespace b/testing/scripts/tests/dwi2mask/mtnorm_whitespace new file mode 100644 index 0000000000..5ca9358b76 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/mtnorm_whitespace @@ -0,0 +1,14 @@ +#!/bin/bash +# Ensure correct operation of the "dwi2mask mtnorm" command +# when image paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask mtnorm "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-tissuesum "tmp tissuesum.mif" + +testing_diff_image "tmp out.mif" dwi2mask/mtnorm_default_mask.mif +testing_diff_image "tmp tissuesum.mif" dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 diff --git a/testing/scripts/tests/dwi2mask/synthstrip_whitespace b/testing/scripts/tests/dwi2mask/synthstrip_whitespace new file mode 100644 index 0000000000..86efab77d1 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/synthstrip_whitespace @@ -0,0 +1,16 @@ +#!/bin/bash +# Ensure correct operation of the "dwi2mask synthstrip" command +# where image paths include whitespace characters +# Check that expected output images appear +rm -f "tmp out.mif" "tmp stripped.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask synthstrip "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-stripped "tmp stripped.mif" + +ls | grep "^tmp out\.mif" +ls | grep "^tmp stripped\.mif" diff --git a/testing/scripts/tests/dwi2mask/trace_whitespace b/testing/scripts/tests/dwi2mask/trace_whitespace new file mode 100644 index 0000000000..df1d62c249 --- /dev/null +++ b/testing/scripts/tests/dwi2mask/trace_whitespace @@ -0,0 +1,11 @@ +#!/bin/bash +# Verify result of "dwi2mask trace" algorithm +# when utilising images that have whitespaces in their paths +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2mask trace "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +testing_diff_image "tmp out.mif" dwi2mask/trace_default.mif.gz diff --git a/testing/scripts/tests/dwi2response/dhollander_whitespace b/testing/scripts/tests/dwi2response/dhollander_whitespace new file mode 100644 index 0000000000..a7d4f21266 --- /dev/null +++ b/testing/scripts/tests/dwi2response/dhollander_whitespace @@ -0,0 +1,17 @@ +#!/bin/bash +# Verify successful execution of "dwi2response dhollander" +# when filesystem paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2response dhollander "tmp in.mif" "tmp wm.txt" "tmp gm.txt" "tmp csf.txt" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-voxels "tmp voxels.mif" + +testing_diff_matrix "tmp wm.txt" dwi2response/dhollander/default_wm.txt -abs 1e-2 +testing_diff_matrix "tmp gm.txt" dwi2response/dhollander/default_gm.txt -abs 1e-2 +testing_diff_matrix "tmp csf.txt" dwi2response/dhollander/default_csf.txt -abs 1e-2 +testing_diff_image "tmp voxels.mif" dwi2response/dhollander/default.mif.gz + diff --git a/testing/scripts/tests/dwi2response/fa_whitespace b/testing/scripts/tests/dwi2response/fa_whitespace new file mode 100644 index 0000000000..7b40b6b07f --- /dev/null +++ b/testing/scripts/tests/dwi2response/fa_whitespace @@ -0,0 +1,16 @@ +#!/bin/bash +# Verify successful execution of "dwi2response fa" +# when filesystem paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2response fa "tmp in.mif" "tmp out.txt" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-voxels "tmp voxels.mif" \ +-number 20 + +testing_diff_matrix "tmp out.txt" dwi2response/fa/default.txt -abs 1e-2 +testing_diff_image "tmp voxels.mif" dwi2response/fa/default.mif.gz + diff --git a/testing/scripts/tests/dwi2response/manual_whitespace b/testing/scripts/tests/dwi2response/manual_whitespace new file mode 100644 index 0000000000..a124963eb6 --- /dev/null +++ b/testing/scripts/tests/dwi2response/manual_whitespace @@ -0,0 +1,20 @@ +#!/bin/bash +# Verify successful execution of "dwi2response manual" +# when filesystem paths include whitespace characters + +dwi2tensor BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz | \ +tensor2metric - -vector "tmp dirs.mif" -force + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s dwi2response/fa/default.mif.gz "tmp voxels.mif.gz" + +dwi2response manual "tmp in.mif" "tmp voxels.mif.gz" "tmp out.txt" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-dirs "tmp dirs.mif" + +testing_diff_matrix "tmp out.txt" dwi2response/manual/dirs.txt -abs 1e-2 + diff --git a/testing/scripts/tests/dwi2response/msmt5tt_whitespace b/testing/scripts/tests/dwi2response/msmt5tt_whitespace new file mode 100644 index 0000000000..32594275f4 --- /dev/null +++ b/testing/scripts/tests/dwi2response/msmt5tt_whitespace @@ -0,0 +1,26 @@ +#!/bin/bash +# Verify successful execution of "dwi2response msmt_5tt" +# when filesystem paths include whitespace characters + +dwi2tensor BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz | \ +tensor2metric - -vector "tmp dirs.mif" -force + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s BIDS/sub-01/anat/sub-01_5TT.nii.gz "tmp 5TT.nii.gz" + +dwi2response msmt_5tt "tmp in.mif" "tmp 5TT.nii.gz" \ +"tmp wm.txt" "tmp gm.txt" "tmp csf.txt" -force \ +-fslgrad "tmp in.bvec" "tmp-in.bval" \ +-dirs "tmp dirs.mif" \ +-voxels "tmp voxels.mif" \ +-pvf 0.9 + +testing_diff_matrix "tmp wm.txt" dwi2response/msmt_5tt/default_wm.txt -abs 1e-2 +testing_diff_matrix "tmp gm.txt" dwi2response/msmt_5tt/default_gm.txt -abs 1e-2 +testing_diff_matrix "tmp csf.txt" dwi2response/msmt_5tt/default_csf.txt -abs 1e-2 +testing_diff_image "tmp voxels.mif" dwi2response/msmt_5tt/default.mif.gz + diff --git a/testing/scripts/tests/dwi2response/tax_whitespace b/testing/scripts/tests/dwi2response/tax_whitespace new file mode 100644 index 0000000000..c49cd5fc41 --- /dev/null +++ b/testing/scripts/tests/dwi2response/tax_whitespace @@ -0,0 +1,15 @@ +#!/bin/bash +# Verify successful execution of "dwi2response tax" +# where image paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2response tax "tmp in.mif" "tmp out.txt" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-voxels "tmp voxels.mif" + +testing_diff_matrix "tmp out.txt" dwi2response/tax/default.txt -abs 1e-2 +testing_diff_image "tmp voxels.mif" dwi2response/tax/default.mif.gz + diff --git a/testing/scripts/tests/dwi2response/tournier_whitespace b/testing/scripts/tests/dwi2response/tournier_whitespace new file mode 100644 index 0000000000..5cc26ca3c7 --- /dev/null +++ b/testing/scripts/tests/dwi2response/tournier_whitespace @@ -0,0 +1,17 @@ +#!/bin/bash +# Verify successful execution of "dwi2response tournier" +# where image paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwi2response tournier "tmp in.mif" "tmp out.txt" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-voxels "tmp voxels.mif" \ +-number 20 \ +-iter_voxels 200 + +testing_diff_matrix "tmp out.txt" dwi2response/tournier/default.txt -abs 1e-2 +testing_diff_image "tmp voxels.mif" dwi2response/tournier/default.mif.gz + diff --git a/testing/scripts/tests/dwibiascorrect/ants_whitespace b/testing/scripts/tests/dwibiascorrect/ants_whitespace new file mode 100644 index 0000000000..8edf806526 --- /dev/null +++ b/testing/scripts/tests/dwibiascorrect/ants_whitespace @@ -0,0 +1,16 @@ +#!/bin/bash +# Verify operation of "dwibiascorrect ants" algorithm +# where image paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" + +dwibiascorrect ants "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-mask "tmp mask.nii.gz" \ +-bias "tmp bias.mif" + +testing_diff_header "tmp out.mif" dwibiascorrect/ants/default_out.mif.gz +testing_diff_header "tmp bias.mif" dwibiascorrect/ants/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect/fsl_whitespace b/testing/scripts/tests/dwibiascorrect/fsl_whitespace new file mode 100644 index 0000000000..bdfa5e5860 --- /dev/null +++ b/testing/scripts/tests/dwibiascorrect/fsl_whitespace @@ -0,0 +1,17 @@ +#!/bin/bash +# Verify operation of "dwibiascorrect fsl" algorithm +# where image paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" + +dwibiascorrect fsl "tmp in.mif" "tmp out.mif" \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-mask "tmp mask.nii.gz" \ +-bias "tmp bias.mif" + +testing_diff_header "tmp out.mif" dwibiascorrect/fsl/default_out.mif.gz +testing_diff_header "tmp bias" dwibiascorrect/fsl/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace b/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace new file mode 100644 index 0000000000..f9b32860ec --- /dev/null +++ b/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace @@ -0,0 +1,16 @@ +#!/bin/bash +# Verify operation of "dwibiascorrect mtnorm" algorithm +# where image paths contain whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" + +dwibiascorrect mtnorm "tmp in.mif" "tmp out.mif" \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-mask "tmp mask.nii.gz" \ +-bias "tmp bias.mif" + +testing_diff_header "tmp out.mif" dwibiascorrect/mtnorm/default_out.mif.gz +testing_diff_header "tmp bias.mif" dwibiascorrect/mtnorm/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiasnormmask/piping b/testing/scripts/tests/dwibiasnormmask/piping index b6680178c3..e9e39e55dc 100644 --- a/testing/scripts/tests/dwibiasnormmask/piping +++ b/testing/scripts/tests/dwibiasnormmask/piping @@ -13,6 +13,11 @@ mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_in.mif -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ -strides 0,0,0,1 +# Input initial mask is a pipe +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | \ +dwibiasnormmask tmp_in.mif tmp_out.mif -force \ +-init_mask - + # Output mask is a pipe dwibiasnormmask tmp_in.mif tmp_out.mif - -force | \ testing_diff_image - dwibiasnormmask/default_mask.mif.gz diff --git a/testing/scripts/tests/dwibiasnormmask/whitespace b/testing/scripts/tests/dwibiasnormmask/whitespace new file mode 100644 index 0000000000..fb98b16833 --- /dev/null +++ b/testing/scripts/tests/dwibiasnormmask/whitespace @@ -0,0 +1,21 @@ +#!/bin/bash +# Verify successful execution of command +# when filesystem paths include whitespace characters +rm -f "tmp *.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" + +dwibiasnormmask "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-init_mask "tmp mask.nii.gz" \ +-output_bias "tmp bias.mif" \ +-output_scale "tmp scale.txt" \ +-output_tissuesum "tmp tissuesum.mif" + +ls | grep "^tmp out\.mif$" +ls | grep "^tmp bias\.mif$" +ls | grep "^tmp scale\.txt$" +ls | grep "^tmp tissuesum\.mif$" diff --git a/testing/scripts/tests/dwicat/whitespace b/testing/scripts/tests/dwicat/whitespace new file mode 100644 index 0000000000..34c11a94aa --- /dev/null +++ b/testing/scripts/tests/dwicat/whitespace @@ -0,0 +1,15 @@ +#!/bin/bash +# Test operation of the dwicat command +# where image paths include whitespace characters +rm -f "tmp out.mif" + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" + +dwicat "tmp in.mif" "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-mask "tmp mask.mif" + +ls | grep "tmp out.mif" diff --git a/testing/scripts/tests/dwifslpreproc/whitespace b/testing/scripts/tests/dwifslpreproc/whitespace new file mode 100644 index 0000000000..d0db8f2f59 --- /dev/null +++ b/testing/scripts/tests/dwifslpreproc/whitespace @@ -0,0 +1,32 @@ +#!/bin/bash +# Ensure that dwifslpreproc executes successfully +# whenever filesystem paths include whitespace characters +rm -rf "tmp *" + +ln -s BIDS/sub-04/dwi/sub-04_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-04/dwi/sub-04_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-04/dwi/sub-04_dwi.bval "tmp in.bval" +ln -s BIDS/sub-04/dwi/sub-04_dwi.json "tmp in.json" + +mrconvert BIDS/sub-04/fmap/sub-04_dir-1_epi.nii.gz tmp1.mif -force \ +-json_import BIDS/sub-04/fmap/sub-04_dir-1_epi.json +mrconvert BIDS/sub-04/fmap/sub-04_dir-2_epi.nii.gz tmp2.mif -force \ +-json_import BIDS/sub-04/fmap/sub-04_dir-2_epi.json +mrcat tmp1.mif tmp2.mif "tmp seepi.mif" -axis 3 -force + +dwi2mask BIDS/sub-04/dwi/sub-04_dwi.nii.gz tmpeddymask.mif -force \ +-fslgrad BIDS/sub-04/dwi/sub-04_dwi.bvec BIDS/sub-04/dwi/sub-04_dwi.bval +ln -s tmpeddymask.mif "tmp eddymask.mif" + +dwifslpreproc "tmp in.nii.gz" "tmp out.nii" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-json_import "tmp in.json" \ +-pe_dir ap -readout_time 0.1 -rpe_pair "tmp seepi.mif" \ +-eddy_mask "tmp eddymask.mif" \ +-eddyqc_test "tmp eddyqc/" \ +-export_grad_fsl "tmp out.bvec" "tmp out.bval" + +ls | grep "^tmp out\.nii$" +ls | grep "^tmp out\.bvec$" +ls | grep "^tmp out\.bval$" +ls | grep "^tmp eddyqc\/$" diff --git a/testing/scripts/tests/dwigradcheck/whitespace b/testing/scripts/tests/dwigradcheck/whitespace new file mode 100644 index 0000000000..2bae2b044c --- /dev/null +++ b/testing/scripts/tests/dwigradcheck/whitespace @@ -0,0 +1,18 @@ +#!/bin/bash +# Ensure that the dwigradcheck script completes successfully +# when filesystem paths include whitespace characters +# Ensure that output filesystem paths appear at the expected locations +rm -f "tmp out.*" + +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwigradcheck "tmp in.mif" \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-mask "tmp mask.nii.gz" \ +-export_grad_fsl "tmp out.bvec" "tmp out.bval" + +ls | grep "^tmp out\.bvec$" +ls | grep "^tmp out\.bval$" diff --git a/testing/scripts/tests/dwinormalise/group_whitespace b/testing/scripts/tests/dwinormalise/group_whitespace new file mode 100644 index 0000000000..f9362df963 --- /dev/null +++ b/testing/scripts/tests/dwinormalise/group_whitespace @@ -0,0 +1,21 @@ +#!/bin/bash +# Test the "dwinormalise group" algorithm +# when filesystem paths include whitespace characters +rm -rf "tmp dwi" "tmp mask" +mkdir "tmp dwi" +mkdir "tmp mask" + +mrconvert BIDS/sub-02/dwi/sub-02_dwi.nii.gz "tmp dwi/sub 02.mif" \ +-fslgrad BIDS/sub-02/dwi/sub-02_dwi.bvec BIDS/sub-02/dwi/sub-02_dwi.bval +mrconvert BIDS/sub-02/dwi/sub-02_brainmask.nii.gz "tmp mask/sub 02.mif" + +mrconvert BIDS/sub-03/dwi/sub-03_dwi.nii.gz "tmp dwi/sub 03.mif" \ +-fslgrad BIDS/sub-03/dwi/sub-03_dwi.bvec BIDS/sub-03/dwi/sub-03_dwi.bval +mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz "tmp mask/sub 03.mif" + +dwinormalise group "tmp dwi/" "tmp mask/" "tmp group/" "tmp template.mif" "tmp mask.mif" -force + +testing_diff_image "tmp template.mif" dwinormalise/group/fa.mif.gz -abs 1e-3 +testing_diff_image $(mrfilter "tmp mask.mif" smooth -) $(mrfilter dwinormalise/group/mask.mif.gz smooth -) -abs 0.3 + +ls | grep "^tmp group\/$" diff --git a/testing/scripts/tests/dwinormalise/manual_whitespace b/testing/scripts/tests/dwinormalise/manual_whitespace new file mode 100644 index 0000000000..2e538af06f --- /dev/null +++ b/testing/scripts/tests/dwinormalise/manual_whitespace @@ -0,0 +1,14 @@ +#!/bin/bash +# Verify default operation of the "dwinormalise manual" algorithm +# where image paths include whitespace characters + +# Input and output DWI series are both piped +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.mif" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" + +dwinormalise manual "tmp in.mif" "tmp mask.nii.gz" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +testing_diff_image "tmp out.mif" dwinormalise/manual/out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwinormalise/mtnorm_whitespace b/testing/scripts/tests/dwinormalise/mtnorm_whitespace new file mode 100644 index 0000000000..b50bd8a140 --- /dev/null +++ b/testing/scripts/tests/dwinormalise/mtnorm_whitespace @@ -0,0 +1,14 @@ +#!/bin/bash +# Verify default operation of the "dwinormalise mtnorm" command +# where filesystem paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" + +dwinormalise mtnorm "tmp in.mif" "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-mask "tmp mask.nii.gz" + +testing_diff_image "tmp out.mif" dwinormalise/mtnorm/masked_out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwishellmath/whitespace b/testing/scripts/tests/dwishellmath/whitespace new file mode 100644 index 0000000000..f30be48601 --- /dev/null +++ b/testing/scripts/tests/dwishellmath/whitespace @@ -0,0 +1,12 @@ +#!/bin/bash +# Verify operation of the dwishellmath command +# when image paths include whitespace characters + +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" +ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" + +dwishellmath "tmp in.mif" mean "tmp out.mif" -force \ +-fslgrad "tmp in.bvec" "tmp in.bval" + +ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/labelsgmfirst/whitespace b/testing/scripts/tests/labelsgmfirst/whitespace new file mode 100644 index 0000000000..35bf1391f3 --- /dev/null +++ b/testing/scripts/tests/labelsgmfirst/whitespace @@ -0,0 +1,11 @@ +#!/bin/bash +# Verify operation of command +# when utilising image paths that include whitespace characters + +ln -s BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz "tmp indices.nii.gz" +ln -s BIDS/sub-01/anat/sub-01_T1w.nii.gz "tmp T1w.nii.gz" +ln -s BIDS/parc-desikan_lookup.txt "tmp lookup.txt" + +labelsgmfirst "tmp indices.nii.gz" "tmp T1w.nii.gz" "tmp lookup.txt" "tmp out.mif" -force + +testing_diff_header "tmp out.mif" labelsgmfirst/default.mif.gz diff --git a/testing/scripts/tests/mask2glass/whitespace b/testing/scripts/tests/mask2glass/whitespace new file mode 100644 index 0000000000..aab638e4e1 --- /dev/null +++ b/testing/scripts/tests/mask2glass/whitespace @@ -0,0 +1,8 @@ +#!/bin/bash +# Verify operation of the "mask2glass" command +# where image paths include whitespace characters +ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp in.nii.gz" + +mask2glass "tmp in.nii.gz" "tmp out.mif" -force + +testing_diff_image "tmp out.mif" mask2glass/out.mif.gz diff --git a/testing/scripts/tests/mrtrix_cleanup/whitespace b/testing/scripts/tests/mrtrix_cleanup/whitespace new file mode 100644 index 0000000000..139d542766 --- /dev/null +++ b/testing/scripts/tests/mrtrix_cleanup/whitespace @@ -0,0 +1,16 @@ +#!/bin/bash +# Verify operation of the command +# when filesystem paths include whitespace characters +rm -rf "tmp in/" +mkdir "tmp in/" + +touch "tmp in/not a tempfile.txt" +touch "tmp in/mrtrix-tmp-ABCDEF.mif" +mkdir "tmp in/not a scratchdir/" +mkdir "tmp in/command-tmp-ABCDEF/" + +mrtrix_cleanup "tmp in/" +ITEMCOUNT=$(ls "tmp in/" | wc) +if [ "$ITEMCOUNT" -neq "2" ]; then + exit 1 +fi diff --git a/testing/scripts/tests/population_template/whitespace b/testing/scripts/tests/population_template/whitespace new file mode 100644 index 0000000000..e73b741328 --- /dev/null +++ b/testing/scripts/tests/population_template/whitespace @@ -0,0 +1,31 @@ +#!/bin/bash +# Evaluate operation of population_template command +# where filesystem paths include whitespace characters + +rm -rf "tmp *" +mkdir "tmp fa" "tmp mask" + +dwi2tensor BIDS/sub-02/dwi/sub-02_dwi.nii.gz - \ +-fslgrad BIDS/sub-02/dwi/sub-02_dwi.bvec BIDS/sub-02/dwi/sub-02_dwi.bval \ +-mask BIDS/sub-02/dwi/sub-02_brainmask.nii.gz | \ +tensor2metric - -fa "tmp fa/sub 02.mif" +dwi2tensor BIDS/sub-03/dwi/sub-03_dwi.nii.gz - \ +-fslgrad BIDS/sub-03/dwi/sub-03_dwi.bvec BIDS/sub-03/dwi/sub-03_dwi.bval \ +-mask BIDS/sub-03/dwi/sub-03_brainmask.nii.gz | \ +tensor2metric - -fa "tmp fa/sub 03.mif" + +mrconvert BIDS/sub-02/dwi/sub-02_brainmask.nii.gz "tmp mask/sub 02.mif" +mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz "tmp mask/sub 03.mif" + +# Output template image is a pipe +population_template "tmp fa/" "tmp template.mif" -force \ +-mask_dir "tmp mask/" \ +-template_mask "tmp mask.mif" \ +-warp_dir "tmp warps/" \ +-linear_transformations_dir "tmp linear_transformations/" + +testing_diff_image "tmp template.mif" population_template/fa_masked_template.mif.gz -abs 0.01 +testing_diff_image $(mrfilter "tmp mask.mif" smooth -) $(mrfilter population_template/fa_masked_mask.mif.gz smooth -) -abs 0.3 + +ls | grep "^tmp warps\/$" +ls | grep "^tmp linear_transformations\/$" diff --git a/testing/scripts/tests/responsemean/whitespace b/testing/scripts/tests/responsemean/whitespace new file mode 100644 index 0000000000..92a5cacf29 --- /dev/null +++ b/testing/scripts/tests/responsemean/whitespace @@ -0,0 +1,7 @@ +#!/bin/bash +# Verify command operation when filesystem paths include whitespace characters +ln -s BIDS/sub-02/dwi/sub-02_tissue-WM_response.txt "tmp response2.txt" +ln -s BIDS/sub-03/dwi/sub-03_tissue-WM_response.txt "tmp response3.txt" + +responsemean "tmp response2txt" "tmp response3.txt" "tmp out.txt" -force +testing_diff_matrix "tmp out.txt" responsemean/out.txt -abs 0.001 From 4095d7665ef2574f7e824603fa00fcedb4f77f92 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 13 May 2024 23:30:59 +1000 Subject: [PATCH 65/75] Testing: Import new test data for new mask2glass tests --- testing/scripts/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/scripts/data b/testing/scripts/data index 4f0d6653e9..76f47633cd 160000 --- a/testing/scripts/data +++ b/testing/scripts/data @@ -1 +1 @@ -Subproject commit 4f0d6653e994212d3653660a4f9163437956c989 +Subproject commit 76f47633cd0a37e901c42320f4540ecaffd51367 From 05b68d536031cd6a387e2e6a00bda2fc7ac4bd7d Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 15 May 2024 00:05:20 +1000 Subject: [PATCH 66/75] Python: Multiple fixes following merge of split tests & CLI changes - Fix syntax error in mask2glass introduced in 99dd92741cf156a094902faac3f990af78b2106e. - population_template: F-string syntax fix. - 5ttgen hsvs: Fix execution when input directory path contains whitespace. - app.Parser: Change type of Exception thrown when filesystem path argument is impermissible, so that argparse catches it. - dwi2mask consensus: Allow execution when the -template option is not specified; just omit from the list any algorithms that depend on such. - dwinormalise group: Fix path to population_template scratch directory. - Testing: Many fixes to individual test bash scripts, particularly around the verification of image pipe and filesystem path whitespace characters. --- python/bin/mask2glass | 2 +- python/bin/population_template | 2 +- python/lib/mrtrix3/_5ttgen/hsvs.py | 94 ++++++++++--------- python/lib/mrtrix3/app.py | 12 +-- python/lib/mrtrix3/dwi2mask/consensus.py | 9 +- python/lib/mrtrix3/dwinormalise/group.py | 2 +- testing/scripts/tests/5ttgen/hsvs_whitespace | 4 +- .../tests/dwi2mask/3dautomask_whitespace | 2 +- testing/scripts/tests/dwi2mask/ants_config | 4 +- testing/scripts/tests/dwi2mask/ants_piping | 2 +- .../scripts/tests/dwi2mask/ants_whitespace | 2 +- .../tests/dwi2mask/b02template_whitespace | 4 +- .../tests/dwi2mask/consensus_whitespace | 2 +- .../scripts/tests/dwi2mask/fslbet_whitespace | 2 +- .../scripts/tests/dwi2mask/hdbet_whitespace | 2 +- .../scripts/tests/dwi2mask/legacy_whitespace | 2 +- .../scripts/tests/dwi2mask/mean_whitespace | 2 +- testing/scripts/tests/dwi2mask/mtnorm_piping | 2 +- .../scripts/tests/dwi2mask/mtnorm_whitespace | 4 +- .../tests/dwi2mask/synthstrip_whitespace | 2 +- testing/scripts/tests/dwi2mask/trace_piping | 2 +- .../scripts/tests/dwi2mask/trace_whitespace | 2 +- .../tests/dwi2response/dhollander_piping | 6 +- .../tests/dwi2response/dhollander_whitespace | 2 +- testing/scripts/tests/dwi2response/fa_piping | 2 +- .../scripts/tests/dwi2response/fa_whitespace | 2 +- .../scripts/tests/dwi2response/manual_piping | 2 +- .../tests/dwi2response/manual_whitespace | 2 +- .../scripts/tests/dwi2response/msmt5tt_piping | 5 +- .../tests/dwi2response/msmt5tt_whitespace | 4 +- .../scripts/tests/dwi2response/tax_fslgrad | 4 +- testing/scripts/tests/dwi2response/tax_grad | 11 +-- testing/scripts/tests/dwi2response/tax_lmax | 7 +- testing/scripts/tests/dwi2response/tax_mask | 4 +- testing/scripts/tests/dwi2response/tax_piping | 2 +- testing/scripts/tests/dwi2response/tax_shell | 6 +- .../scripts/tests/dwi2response/tax_whitespace | 2 +- .../tests/dwi2response/tournier_fslgrad | 8 +- .../scripts/tests/dwi2response/tournier_grad | 13 ++- .../scripts/tests/dwi2response/tournier_lmax | 8 +- .../scripts/tests/dwi2response/tournier_mask | 8 +- .../scripts/tests/dwi2response/tournier_shell | 8 +- .../tests/dwi2response/tournier_whitespace | 2 +- .../tests/dwibiascorrect/ants_whitespace | 2 +- .../tests/dwibiascorrect/fsl_whitespace | 4 +- .../scripts/tests/dwibiascorrect/mtnorm_lmax | 2 +- .../tests/dwibiascorrect/mtnorm_whitespace | 2 +- testing/scripts/tests/dwibiasnormmask/piping | 6 +- .../scripts/tests/dwibiasnormmask/reference | 3 +- .../scripts/tests/dwibiasnormmask/whitespace | 3 +- testing/scripts/tests/dwicat/piping | 7 +- testing/scripts/tests/dwicat/whitespace | 9 +- testing/scripts/tests/dwifslpreproc/piping | 1 + .../tests/dwifslpreproc/rpenone_default | 2 +- .../scripts/tests/dwifslpreproc/whitespace | 9 +- testing/scripts/tests/dwigradcheck/whitespace | 2 +- .../scripts/tests/dwinormalise/group_default | 2 +- .../scripts/tests/dwinormalise/manual_piping | 2 +- .../tests/dwinormalise/manual_whitespace | 4 +- .../tests/dwinormalise/mtnorm_whitespace | 2 +- testing/scripts/tests/dwishellmath/whitespace | 2 +- testing/scripts/tests/labelsgmfirst/piping | 4 +- 62 files changed, 176 insertions(+), 161 deletions(-) diff --git a/python/bin/mask2glass b/python/bin/mask2glass index 80c88b3e3a..439a9e2ade 100755 --- a/python/bin/mask2glass +++ b/python/bin/mask2glass @@ -58,7 +58,7 @@ def execute(): #pylint: disable=unused-variable from mrtrix3 import app, image, run #pylint: disable=no-name-in-module, import-outside-toplevel image_size = image.Header(app.ARGS.input).size() - if not len(image_size) == 3 or (len(image_size == 4) and image_size[-1] == 1): + if not len(image_size) == 3 or (len(image_size) == 4 and image_size[-1] == 1): raise MRtrixError('Command expects as input a 3D image') # import data to scratch directory diff --git a/python/bin/population_template b/python/bin/population_template index 2bb27e0234..5554f86efa 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -1488,7 +1488,7 @@ def execute(): #pylint: disable=unused-variable 'average', f'linear_transform_average_{level:02d}.txt', '-quiet']) - transform_average_driftref = matrix.load_transform(f'linear_transform_average_{level:%02i}.txt') + transform_average_driftref = matrix.load_transform(f'linear_transform_average_{level:02d}.txt') else: run.command(['transformcalc', [os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt') for inp in ins], diff --git a/python/lib/mrtrix3/_5ttgen/hsvs.py b/python/lib/mrtrix3/_5ttgen/hsvs.py index 72bb0f146c..c1464c4ab5 100644 --- a/python/lib/mrtrix3/_5ttgen/hsvs.py +++ b/python/lib/mrtrix3/_5ttgen/hsvs.py @@ -205,7 +205,7 @@ def execute(): #pylint: disable=unused-variable # Use brain-extracted, bias-corrected image for FSL tools norm_image = os.path.join(mri_dir, 'norm.mgz') check_file(norm_image) - run.command(f'mrconvert {norm_image} T1.nii -stride -1,+2,+3') + run.command(['mrconvert', norm_image, 'T1.nii', '-stride', '-1,+2,+3']) # Verify FAST availability try: fast_cmd = fsl.exe_name('fast') @@ -406,8 +406,8 @@ def execute(): #pylint: disable=unused-variable for (index, tissue, name) in from_aseg: init_mesh_path = f'{name}_init.vtk' smoothed_mesh_path = f'{name}.vtk' - run.command(f'mrcalc {aparc_image} {index} -eq - | ' - f'voxel2mesh - -threshold 0.5 {init_mesh_path}') + run.command(['mrcalc', aparc_image, f'{index}', '-eq', '-', '|', + 'voxel2mesh', '-', '-threshold', '0.5', init_mesh_path]) run.command(['meshfilter', init_mesh_path, 'smooth', smoothed_mesh_path]) app.cleanup(init_mesh_path) run.command(['mesh2voxel', smoothed_mesh_path, template_image, f'{name}.mif']) @@ -434,7 +434,7 @@ def execute(): #pylint: disable=unused-variable # Combine corpus callosum segments before smoothing progress = app.ProgressBar('Combining and smoothing corpus callosum segmentation', len(CORPUS_CALLOSUM_ASEG) + 3) for (index, name) in CORPUS_CALLOSUM_ASEG: - run.command(f'mrcalc {aparc_image} {index} -eq {name}.mif -datatype bit') + run.command(['mrcalc', aparc_image, f'{index}', '-eq', f'{name}.mif', '-datatype', 'bit']) progress.increment() cc_init_mesh_path = 'combined_corpus_callosum_init.vtk' cc_smoothed_mesh_path = 'combined_corpus_callosum.vtk' @@ -459,9 +459,11 @@ def execute(): #pylint: disable=unused-variable bs_fullmask_path = 'brain_stem_init.mif' bs_cropmask_path = '' progress = app.ProgressBar('Segmenting and cropping brain stem', 5) - run.command(f'mrcalc {aparc_image} {BRAIN_STEM_ASEG[0][0]} -eq ' - + ' -add '.join([ f'{aparc_image} {index} -eq' for index, name in BRAIN_STEM_ASEG[1:] ]) - + f' -add {bs_fullmask_path} -datatype bit') + cmd = ['mrcalc', aparc_image, BRAIN_STEM_ASEG[0][0], '-eq'] + for index, name in BRAIN_STEM_ASEG[1:]: + cmd.extend([aparc_image, f'{index}', '-eq', '-add']) + cmd.extend([bs_fullmask_path, '-datatype', 'bit']) + run.command(cmd) progress.increment() bs_init_mesh_path = 'brain_stem_init.vtk' run.command(['voxel2mesh', bs_fullmask_path, bs_init_mesh_path]) @@ -502,8 +504,8 @@ def execute(): #pylint: disable=unused-variable init_mesh_path = f'{hemi}_{structure_name}_subfield_{tissue}_init.vtk' smooth_mesh_path = f'{hemi}_{structure_name}_subfield_{tissue}.vtk' subfield_tissue_image = f'{hemi}_{structure_name}_subfield_{tissue}.mif' - run.command(f'mrcalc {subfields_all_tissues_image} {tissue+1} -eq - | ' - f'voxel2mesh - {init_mesh_path}') + run.command(['mrcalc', subfields_all_tissues_image, f'{tissue+1}', '-eq', '-', '|', + 'voxel2mesh', '-', init_mesh_path]) progress.increment() # Since the hippocampal subfields segmentation can include some fine structures, reduce the extent of smoothing run.command(['meshfilter', init_mesh_path, 'smooth', smooth_mesh_path, @@ -616,10 +618,10 @@ def voxel2scanner(voxel, header): # Generate the mask image within which FAST will be run acpc_prefix = 'ACPC' if ATTEMPT_PC else 'AC' acpc_mask_image = f'{acpc_prefix}_FAST_mask.mif' - run.command(f'mrcalc {template_image} nan -eq - | ' - f'mredit - {acpc_mask_image} -scanner ' - + '-sphere ' + ','.join(map(str, ac_scanner)) + ' 8 1 ' - + ('-sphere ' + ','.join(map(str, pc_scanner)) + ' 5 1' if ATTEMPT_PC else '')) + run.command(['mrcalc', template_image, 'nan', '-eq', '-', '|', + 'mredit', '-', acpc_mask_image, '-scanner', + '-sphere', ','.join(map(str, ac_scanner)), '8', '1'] + + (['-sphere', ','.join(map(str, pc_scanner)), '5', '1'] if ATTEMPT_PC else [])) progress.increment() acpc_t1_masked_image = f'{acpc_prefix}_T1.nii' @@ -652,17 +654,17 @@ def voxel2scanner(voxel, header): for hemi in [ 'Left-', 'Right-' ]: wm_index = [ index for index, tissue, name in CEREBELLUM_ASEG if name.startswith(hemi) and 'White' in name ][0] gm_index = [ index for index, tissue, name in CEREBELLUM_ASEG if name.startswith(hemi) and 'Cortex' in name ][0] - run.command(f'mrcalc {aparc_image} {wm_index} -eq {aparc_image} {gm_index} -eq -add - | ' - f'voxel2mesh - {hemi}cerebellum_all_init.vtk') + run.command(['mrcalc', aparc_image, wm_index, '-eq', aparc_image, gm_index, '-eq', '-add', '-', '|', + 'voxel2mesh', '-', f'{hemi}cerebellum_all_init.vtk']) progress.increment() - run.command(f'mrcalc {aparc_image} {gm_index} -eq - | ' - f'voxel2mesh - {hemi}cerebellum_grey_init.vtk') + run.command(['mrcalc', aparc_image, gm_index, '-eq', '-', '|', + 'voxel2mesh', '-', f'{hemi}cerebellum_grey_init.vtk']) progress.increment() for name, tissue in { 'all':2, 'grey':1 }.items(): - run.command(f'meshfilter {hemi}cerebellum_{name}_init.vtk smooth {hemi}cerebellum_{name}.vtk') + run.command(['meshfilter', f'{hemi}cerebellum_{name}_init.vtk', 'smooth', f'{hemi}cerebellum_{name}.vtk']) app.cleanup(f'{hemi}cerebellum_{name}_init.vtk') progress.increment() - run.command(f'mesh2voxel {hemi}cerebellum_{name}.vtk {template_image} {hemi}cerebellum_{name}.mif') + run.command(['mesh2voxel', f'{hemi}cerebellum_{name}.vtk', template_image, f'{hemi}cerebellum_{name}.mif']) app.cleanup(f'{hemi}cerebellum_{name}.vtk') progress.increment() tissue_images[tissue].append(f'{hemi}cerebellum_{name}.mif') @@ -687,24 +689,24 @@ def voxel2scanner(voxel, header): tissue_images = [ 'tissue0.mif', 'tissue1.mif', 'tissue2.mif', 'tissue3.mif', 'tissue4.mif' ] run.function(os.rename, 'tissue4_init.mif', 'tissue4.mif') progress.increment() - run.command(f'mrcalc tissue3_init.mif tissue3_init.mif {tissue_images[4]} -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[3]}') + run.command(['mrcalc', 'tissue3_init.mif', 'tissue3_init.mif', tissue_images[4], '-add', '1.0', '-sub', '0.0', '-max', '-sub', '0.0', '-max', tissue_images[3]]) app.cleanup('tissue3_init.mif') progress.increment() run.command(['mrmath', tissue_images[3:5], 'sum', 'tissuesum_34.mif']) progress.increment() - run.command(f'mrcalc tissue1_init.mif tissue1_init.mif tissuesum_34.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[1]}') + run.command(['mrcalc', 'tissue1_init.mif', 'tissue1_init.mif', 'tissuesum_34.mif', '-add', '1.0', '-sub', '0.0', '-max', '-sub', '0.0', '-max', tissue_images[1]]) app.cleanup('tissue1_init.mif') app.cleanup('tissuesum_34.mif') progress.increment() run.command(['mrmath', tissue_images[1], tissue_images[3:5], 'sum', 'tissuesum_134.mif']) progress.increment() - run.command(f'mrcalc tissue2_init.mif tissue2_init.mif tissuesum_134.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[2]}') + run.command(['mrcalc', 'tissue2_init.mif', 'tissue2_init.mif', 'tissuesum_134.mif', '-add', '1.0', '-sub', '0.0', '-max', '-sub', '0.0', '-max', tissue_images[2]]) app.cleanup('tissue2_init.mif') app.cleanup('tissuesum_134.mif') progress.increment() run.command(['mrmath', tissue_images[1:5], 'sum', 'tissuesum_1234.mif']) progress.increment() - run.command(f'mrcalc tissue0_init.mif tissue0_init.mif tissuesum_1234.mif -add 1.0 -sub 0.0 -max -sub 0.0 -max {tissue_images[0]}') + run.command(['mrcalc', 'tissue0_init.mif', 'tissue0_init.mif', 'tissuesum_1234.mif', '-add', '1.0', '-sub', '0.0', '-max', '-sub', '0.0', '-max', tissue_images[0]]) app.cleanup('tissue0_init.mif') app.cleanup('tissuesum_1234.mif') progress.increment() @@ -747,10 +749,12 @@ def voxel2scanner(voxel, header): smooth_mesh_path = f'{hemi}-Cerebellum-All-Smooth.vtk' pvf_image_path = f'{hemi}-Cerebellum-PVF-Template.mif' cerebellum_aseg_hemi = [ entry for entry in CEREBELLUM_ASEG if hemi in entry[2] ] - run.command(f'mrcalc {aparc_image} {cerebellum_aseg_hemi[0][0]} -eq ' - + ' -add '.join([ aparc_image + ' ' + str(index) + ' -eq' for index, tissue, name in cerebellum_aseg_hemi[1:] ]) - + ' -add - | ' - f'voxel2mesh - {init_mesh_path}') + cmd = ['mrcalc', aparc_image, cerebellum_aseg_hemi[0][0], '-eq'] + for index, tissue, name in cerebellum_aseg_hemi[1:]: + cmd.extend([aparc_image, f'{index}', '-eq', '-add']) + cmd.extend(['-', '|', + 'voxel2mesh', '-', init_mesh_path]) + run.command(cmd) progress.increment() run.command(['meshfilter', init_mesh_path, 'smooth', smooth_mesh_path]) app.cleanup(init_mesh_path) @@ -775,9 +779,11 @@ def voxel2scanner(voxel, header): else: app.console('Preparing images of cerebellum for intensity-based segmentation') - run.command(f'mrcalc {aparc_image} {CEREBELLUM_ASEG[0][0]} -eq ' - + ' -add '.join([ f'{aparc_image} {index} -eq' for index, tissue, name in CEREBELLUM_ASEG[1:] ]) - + f' -add {cerebellum_volume_image}') + cmd = ['mrcalc', aparc_image, CEREBELLUM_ASEG[0][0], '-eq'] + for index, tissue, name in CEREBELLUM_ASEG[1:]: + cmd.extend([aparc_image, f'{index}', '-eq', '-add']) + cmd.append(cerebellum_volume_image) + run.command(cmd) cerebellum_mask_image = cerebellum_volume_image run.command(['mrcalc', 'T1.nii', cerebellum_mask_image, '-mult', t1_cerebellum_masked]) @@ -827,21 +833,21 @@ def voxel2scanner(voxel, header): new_tissue_images = [ 'tissue0_fast.mif', 'tissue1_fast.mif', 'tissue2_fast.mif', 'tissue3_fast.mif', 'tissue4_fast.mif' ] new_tissue_sum_image = 'tissuesum_01234_fast.mif' cerebellum_multiplier_image = 'Cerebellar_multiplier.mif' - run.command(f'mrcalc {cerebellum_volume_image} {tissue_sum_image} -add 0.5 -gt 1.0 {tissue_sum_image} -sub 0.0 -if {cerebellum_multiplier_image}') + run.command(['mrcalc', cerebellum_volume_image, tissue_sum_image, '-add', '0.5', '-gt', '1.0', tissue_sum_image, '-sub', '0.0', '-if', cerebellum_multiplier_image]) app.cleanup(cerebellum_volume_image) progress.increment() run.command(['mrconvert', tissue_images[0], new_tissue_images[0]]) app.cleanup(tissue_images[0]) progress.increment() - run.command(f'mrcalc {tissue_images[1]} {cerebellum_multiplier_image} {fast_outputs_template[1]} -mult -add {new_tissue_images[1]}') + run.command(['mrcalc', tissue_images[1], cerebellum_multiplier_image, fast_outputs_template[1], '-mult', '-add', new_tissue_images[1]]) app.cleanup(tissue_images[1]) app.cleanup(fast_outputs_template[1]) progress.increment() - run.command(f'mrcalc {tissue_images[2]} {cerebellum_multiplier_image} {fast_outputs_template[2]} -mult -add {new_tissue_images[2]}') + run.command(['mrcalc', tissue_images[2], cerebellum_multiplier_image, fast_outputs_template[2], '-mult', '-add', new_tissue_images[2]]) app.cleanup(tissue_images[2]) app.cleanup(fast_outputs_template[2]) progress.increment() - run.command(f'mrcalc {tissue_images[3]} {cerebellum_multiplier_image} {fast_outputs_template[0]} -mult -add {new_tissue_images[3]}') + run.command(['mrcalc', tissue_images[3], cerebellum_multiplier_image, fast_outputs_template[0], '-mult', '-add', new_tissue_images[3]]) app.cleanup(tissue_images[3]) app.cleanup(fast_outputs_template[0]) app.cleanup(cerebellum_multiplier_image) @@ -873,13 +879,13 @@ def voxel2scanner(voxel, header): f'{os.path.splitext(tissue_images[3])[0]}_filled.mif', tissue_images[4] ] csf_fill_image = 'csf_fill.mif' - run.command(f'mrcalc 1.0 {tissue_sum_image} -sub {tissue_sum_image} 0.0 -gt {mask_image} -add 1.0 -min -mult 0.0 -max {csf_fill_image}') + run.command(['mrcalc', '1.0', tissue_sum_image, '-sub', tissue_sum_image, '0.0', '-gt', mask_image, '-add', '1.0', '-min', '-mult', '0.0', '-max', csf_fill_image]) app.cleanup(tissue_sum_image) # If no template is specified, this file is part of the FreeSurfer output; hence don't modify if app.ARGS.template: app.cleanup(mask_image) progress.increment() - run.command(f'mrcalc {tissue_images[3]} {csf_fill_image} -add {new_tissue_images[3]}') + run.command(['mrcalc', tissue_images[3], csf_fill_image, '-add', new_tissue_images[3]]) app.cleanup(csf_fill_image) app.cleanup(tissue_images[3]) progress.done() @@ -897,13 +903,13 @@ def voxel2scanner(voxel, header): f'{os.path.splitext(tissue_images[2])[0]}_no_brainstem.mif', tissue_images[3], f'{os.path.splitext(tissue_images[4])[0]}_with_brainstem.mif' ] - run.command(f'mrcalc {tissue_images[2]} brain_stem.mif -min brain_stem_white_overlap.mif') + run.command(['mrcalc', tissue_images[2], 'brain_stem.mif', '-min', 'brain_stem_white_overlap.mif']) app.cleanup('brain_stem.mif') progress.increment() - run.command(f'mrcalc {tissue_images[2]} brain_stem_white_overlap.mif -sub {new_tissue_images[2]}') + run.command(['mrcalc', tissue_images[2], 'brain_stem_white_overlap.mif', '-sub', new_tissue_images[2]]) app.cleanup(tissue_images[2]) progress.increment() - run.command(f'mrcalc {tissue_images[4]} brain_stem_white_overlap.mif -add {new_tissue_images[4]}') + run.command(['mrcalc', tissue_images[4], 'brain_stem_white_overlap.mif', '-add', new_tissue_images[4]]) app.cleanup(tissue_images[4]) app.cleanup('brain_stem_white_overlap.mif') progress.done() @@ -930,11 +936,11 @@ def voxel2scanner(voxel, header): else: app.console('Cropping final 5TT image') crop_mask_image = 'crop_mask.mif' - run.command(f'mrconvert {precrop_result_image} -coord 3 0,1,2,4 - | ' - f'mrmath - sum - -axis 3 | ' - f'mrthreshold - - -abs 0.001 | ' - f'maskfilter - dilate {crop_mask_image}') - run.command(f'mrgrid {precrop_result_image} crop result.mif -mask {crop_mask_image}') + run.command(['mrconvert', precrop_result_image, '-coord', '3', '0,1,2,4', '-', '|', + 'mrmath', '-', 'sum', '-', '-axis', '3', '|', + 'mrthreshold', '-', '-', '-abs', '0.001', '|', + 'maskfilter', '-', 'dilate', crop_mask_image]) + run.command(['mrgrid', precrop_result_image, 'crop', 'result.mif', '-mask', crop_mask_image]) app.cleanup(crop_mask_image) app.cleanup(precrop_result_image) diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index d53e9006a9..0c6a4f4055 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -604,8 +604,8 @@ def check_output(self, item_type='path'): warn(f'Output {item_type} "{str(self)}" already exists; ' 'will be overwritten at script completion') else: - raise MRtrixError(f'Output {item_type} "{str(self)}" already exists ' - '(use -force option to force overwrite)') + raise argparse.ArgumentError(f'Output {item_type} "{str(self)}" already exists ' + '(use -force option to force overwrite)') class _UserFileOutPathExtras(_UserOutPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) @@ -630,8 +630,8 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ return except FileExistsError: if not FORCE_OVERWRITE: - raise MRtrixError(f'Output directory "{str(self)}" already exists ' # pylint: disable=raise-missing-from - '(use -force option to force overwrite)') + raise argparse.ArgumentError(f'Output directory "{str(self)}" already exists ' # pylint: disable=raise-missing-from + '(use -force option to force overwrite)') # Various callable types for use as argparse argument types class CustomTypeBase: @@ -918,9 +918,9 @@ def __init__(self, *args_in, **kwargs_in): help='do not delete intermediate files during script execution, ' 'and do not delete scratch directory at script completion.') script_options.add_argument('-scratch', - type=Parser.DirectoryOut(), + type=Parser.DirectoryIn(), metavar='/path/to/scratch/', - help='manually specify the path in which to generate the scratch directory.') + help='manually specify an existing directory in which to generate the scratch directory.') script_options.add_argument('-continue', type=Parser.Various(), nargs=2, diff --git a/python/lib/mrtrix3/dwi2mask/consensus.py b/python/lib/mrtrix3/dwi2mask/consensus.py index 906dfb0e78..228367bfc3 100644 --- a/python/lib/mrtrix3/dwi2mask/consensus.py +++ b/python/lib/mrtrix3/dwi2mask/consensus.py @@ -77,7 +77,6 @@ def execute(): #pylint: disable=unused-variable algorithm_list = [item for item in algorithm_list if item != 'b02template'] algorithm_list.append('b02template -software antsfull') algorithm_list.append('b02template -software fsl') - app.debug(str(algorithm_list)) if any(any(item in alg for item in ('ants', 'b02template')) for alg in algorithm_list): if app.ARGS.template: @@ -90,9 +89,15 @@ def execute(): #pylint: disable=unused-variable '-strides', '+1,+2,+3']) run.command(['mrconvert', CONFIG['Dwi2maskTemplateMask'], 'template_mask.nii', '-strides', '+1,+2,+3', '-datatype', 'uint8']) - else: + elif app.ARGS.algorithms: raise MRtrixError('Cannot include within consensus algorithms that necessitate use of a template image ' 'if no template image is provided via command-line or configuration file') + else: + app.warn('No template image and mask provided;' + ' algorithms based on registration to template will be excluded from consensus') + algorithm_list = [item for item in algorithm_list if (item != 'ants' and not item.startswith('b02template'))] + + app.debug(str(algorithm_list)) mask_list = [] for alg in algorithm_list: diff --git a/python/lib/mrtrix3/dwinormalise/group.py b/python/lib/mrtrix3/dwinormalise/group.py index 04a62a9aa0..e7c2dd9e53 100644 --- a/python/lib/mrtrix3/dwinormalise/group.py +++ b/python/lib/mrtrix3/dwinormalise/group.py @@ -115,7 +115,7 @@ def __init__(self, filename, prefix, mask_filename = ''): '-nl_niter', '5,5,5,5,5', '-warp_dir', 'warps', '-linear_no_pause', - '-scratch', 'population_template'] + '-scratch', app.SCRATCH_DIR] + ([] if app.DO_CLEANUP else ['-nocleanup'])) app.console('Generating WM mask in template space') diff --git a/testing/scripts/tests/5ttgen/hsvs_whitespace b/testing/scripts/tests/5ttgen/hsvs_whitespace index e92274a9b9..0b5122ca45 100644 --- a/testing/scripts/tests/5ttgen/hsvs_whitespace +++ b/testing/scripts/tests/5ttgen/hsvs_whitespace @@ -1,6 +1,6 @@ #!/bin/bash # Ensure correct operation of the "5ttgen hsvs" command # when filesystem paths include whitespace characters -ln -s "tmp in/" freesurfer/sub-01 -5ttgen hsvs "tmp in/" "tmp out.mif" -force +ln -s freesurfer/sub-01 "tmp in" +5ttgen hsvs "tmp in" "tmp out.mif" -force testing_diff_header "tmp out.mif" 5ttgen/hsvs/default.mif.gz diff --git a/testing/scripts/tests/dwi2mask/3dautomask_whitespace b/testing/scripts/tests/dwi2mask/3dautomask_whitespace index 21e7fcfc70..900de181f4 100644 --- a/testing/scripts/tests/dwi2mask/3dautomask_whitespace +++ b/testing/scripts/tests/dwi2mask/3dautomask_whitespace @@ -8,7 +8,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask 3dautomask "tmp in.mif" "tmp out.mif" -force \ +dwi2mask 3dautomask "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/ants_config b/testing/scripts/tests/dwi2mask/ants_config index d5e6c8faf2..9fd423615a 100644 --- a/testing/scripts/tests/dwi2mask/ants_config +++ b/testing/scripts/tests/dwi2mask/ants_config @@ -7,5 +7,5 @@ # simulate this by using the standard option "-config" to modify the configuration just for this one invocation dwi2mask ants BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp.mif -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --config Dwi2maskTemplateImage dwi2mask/template_image.mif.gz \ --config Dwi2maskTemplateMask dwi2mask/template_mask.mif.gz +-config Dwi2maskTemplateImage $(dirname $(dirname $(dirname $(realpath "$0"))))/data/dwi2mask/template_image.mif.gz \ +-config Dwi2maskTemplateMask $(dirname $(dirname $(dirname $(realpath "$0"))))/data/dwi2mask/template_mask.mif.gz diff --git a/testing/scripts/tests/dwi2mask/ants_piping b/testing/scripts/tests/dwi2mask/ants_piping index b327d7868f..2349920b6e 100644 --- a/testing/scripts/tests/dwi2mask/ants_piping +++ b/testing/scripts/tests/dwi2mask/ants_piping @@ -3,7 +3,7 @@ # when utilising image pipes # Input and output images are pipes -mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - | +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwi2mask ants - - \ -template dwi2mask/template_image.mif.gz dwi2mask/template_mask.mif.gz | \ diff --git a/testing/scripts/tests/dwi2mask/ants_whitespace b/testing/scripts/tests/dwi2mask/ants_whitespace index 2259c3b321..10b97f16dc 100644 --- a/testing/scripts/tests/dwi2mask/ants_whitespace +++ b/testing/scripts/tests/dwi2mask/ants_whitespace @@ -11,7 +11,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" ln -s dwi2mask/template_mask.mif.gz "tmp mask.mif.gz" -dwi2mask ants "tmp in.mif" "tmp out.mif" -force \ +dwi2mask ants "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -template "tmp template.mif.gz" "tmp mask.mif.gz" diff --git a/testing/scripts/tests/dwi2mask/b02template_whitespace b/testing/scripts/tests/dwi2mask/b02template_whitespace index 9d29ced452..32039bac82 100644 --- a/testing/scripts/tests/dwi2mask/b02template_whitespace +++ b/testing/scripts/tests/dwi2mask/b02template_whitespace @@ -9,9 +9,9 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" -ln -s dwi2mask/template_mask.mif.hz "tmp mask.mif.gz" +ln -s dwi2mask/template_mask.mif.gz "tmp mask.mif.gz" -dwi2mask b02template "tmp in.mif" "tmp out.mif" -force \ +dwi2mask b02template "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -software antsquick \ -template "tmp template.mif.gz" "tmp mask.mif.gz" diff --git a/testing/scripts/tests/dwi2mask/consensus_whitespace b/testing/scripts/tests/dwi2mask/consensus_whitespace index 43e1e35dbd..e04994e903 100644 --- a/testing/scripts/tests/dwi2mask/consensus_whitespace +++ b/testing/scripts/tests/dwi2mask/consensus_whitespace @@ -11,7 +11,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask consensus "tmp in.mif" "tmp out.mif" -force \ +dwi2mask consensus "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -template "tmp template.mif.gz" "tmp mask.mif.gz" \ -masks "tmp masks.mif" diff --git a/testing/scripts/tests/dwi2mask/fslbet_whitespace b/testing/scripts/tests/dwi2mask/fslbet_whitespace index 1ce901ed3b..795c2e6e89 100644 --- a/testing/scripts/tests/dwi2mask/fslbet_whitespace +++ b/testing/scripts/tests/dwi2mask/fslbet_whitespace @@ -8,7 +8,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask fslbet "tmp in.mif" "tmp out.mif" -force \ +dwi2mask fslbet "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/hdbet_whitespace b/testing/scripts/tests/dwi2mask/hdbet_whitespace index bae7c94c88..2e20a91312 100644 --- a/testing/scripts/tests/dwi2mask/hdbet_whitespace +++ b/testing/scripts/tests/dwi2mask/hdbet_whitespace @@ -8,7 +8,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask hdbet "tmp in.mif" "tmp out.mif" -force \ +dwi2mask hdbet "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/legacy_whitespace b/testing/scripts/tests/dwi2mask/legacy_whitespace index 4e544d30e4..2b1d314555 100644 --- a/testing/scripts/tests/dwi2mask/legacy_whitespace +++ b/testing/scripts/tests/dwi2mask/legacy_whitespace @@ -5,7 +5,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask legacy "tmp in.mif" "tmp out.mif" -force \ +dwi2mask legacy "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" testing_diff_image "tmp out.mif" dwi2mask/legacy.mif.gz diff --git a/testing/scripts/tests/dwi2mask/mean_whitespace b/testing/scripts/tests/dwi2mask/mean_whitespace index 5fb5651816..a284a0c1fb 100644 --- a/testing/scripts/tests/dwi2mask/mean_whitespace +++ b/testing/scripts/tests/dwi2mask/mean_whitespace @@ -5,7 +5,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask mean "tmp in.mif" "tmp out.mif" -force \ +dwi2mask mean "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" testing_diff_image "tmp out.mif" dwi2mask/mean.mif.gz diff --git a/testing/scripts/tests/dwi2mask/mtnorm_piping b/testing/scripts/tests/dwi2mask/mtnorm_piping index 76af9ccdeb..a1134e33af 100644 --- a/testing/scripts/tests/dwi2mask/mtnorm_piping +++ b/testing/scripts/tests/dwi2mask/mtnorm_piping @@ -6,7 +6,7 @@ mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwi2mask mtnorm - - | \ -testing_diff_image - dwi2mask/mtnorm_default_mask.mif +testing_diff_image - dwi2mask/mtnorm_default_mask.mif.gz # Output tissue sum image is piped diff --git a/testing/scripts/tests/dwi2mask/mtnorm_whitespace b/testing/scripts/tests/dwi2mask/mtnorm_whitespace index 5ca9358b76..72762e9aff 100644 --- a/testing/scripts/tests/dwi2mask/mtnorm_whitespace +++ b/testing/scripts/tests/dwi2mask/mtnorm_whitespace @@ -6,9 +6,9 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask mtnorm "tmp in.mif" "tmp out.mif" -force \ +dwi2mask mtnorm "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -tissuesum "tmp tissuesum.mif" -testing_diff_image "tmp out.mif" dwi2mask/mtnorm_default_mask.mif +testing_diff_image "tmp out.mif" dwi2mask/mtnorm_default_mask.mif.gz testing_diff_image "tmp tissuesum.mif" dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 diff --git a/testing/scripts/tests/dwi2mask/synthstrip_whitespace b/testing/scripts/tests/dwi2mask/synthstrip_whitespace index 86efab77d1..b90a93addd 100644 --- a/testing/scripts/tests/dwi2mask/synthstrip_whitespace +++ b/testing/scripts/tests/dwi2mask/synthstrip_whitespace @@ -8,7 +8,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask synthstrip "tmp in.mif" "tmp out.mif" -force \ +dwi2mask synthstrip "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -stripped "tmp stripped.mif" diff --git a/testing/scripts/tests/dwi2mask/trace_piping b/testing/scripts/tests/dwi2mask/trace_piping index c02cfc38e0..e45461495f 100644 --- a/testing/scripts/tests/dwi2mask/trace_piping +++ b/testing/scripts/tests/dwi2mask/trace_piping @@ -1,7 +1,7 @@ #!/bin/bash # Verify result of "dwi2mask trace" algorithm # when utilising image pipes -mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - | +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwi2mask trace - - | \ testing_diff_image - dwi2mask/trace_default.mif.gz diff --git a/testing/scripts/tests/dwi2mask/trace_whitespace b/testing/scripts/tests/dwi2mask/trace_whitespace index df1d62c249..a6b4101c5b 100644 --- a/testing/scripts/tests/dwi2mask/trace_whitespace +++ b/testing/scripts/tests/dwi2mask/trace_whitespace @@ -5,7 +5,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2mask trace "tmp in.mif" "tmp out.mif" -force \ +dwi2mask trace "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" testing_diff_image "tmp out.mif" dwi2mask/trace_default.mif.gz diff --git a/testing/scripts/tests/dwi2response/dhollander_piping b/testing/scripts/tests/dwi2response/dhollander_piping index 5698dd2719..c92f007d4a 100644 --- a/testing/scripts/tests/dwi2response/dhollander_piping +++ b/testing/scripts/tests/dwi2response/dhollander_piping @@ -13,5 +13,7 @@ testing_diff_image - dwi2response/dhollander/default.mif.gz # Brain mask is piped mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | \ dwi2response dhollander BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_wm.txt tmp_gm.txt tmp_csf.txt -force \ --mask - | \ -testing_diff_image - dwi2response/dhollander/masked.mif.gz +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-voxels tmp.mif \ +-mask - +testing_diff_image tmp.mif dwi2response/dhollander/masked.mif.gz diff --git a/testing/scripts/tests/dwi2response/dhollander_whitespace b/testing/scripts/tests/dwi2response/dhollander_whitespace index a7d4f21266..a93023e362 100644 --- a/testing/scripts/tests/dwi2response/dhollander_whitespace +++ b/testing/scripts/tests/dwi2response/dhollander_whitespace @@ -6,7 +6,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2response dhollander "tmp in.mif" "tmp wm.txt" "tmp gm.txt" "tmp csf.txt" -force \ +dwi2response dhollander "tmp in.nii.gz" "tmp wm.txt" "tmp gm.txt" "tmp csf.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -voxels "tmp voxels.mif" diff --git a/testing/scripts/tests/dwi2response/fa_piping b/testing/scripts/tests/dwi2response/fa_piping index 6549f7ea95..bb58d79a4f 100644 --- a/testing/scripts/tests/dwi2response/fa_piping +++ b/testing/scripts/tests/dwi2response/fa_piping @@ -6,6 +6,6 @@ mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -strides 0,0,0,1 \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwi2response fa - tmp_out.txt -force \ --voxels tmp_voxels.mif \ +-voxels - \ -number 20 | \ testing_diff_image - dwi2response/fa/default.mif.gz diff --git a/testing/scripts/tests/dwi2response/fa_whitespace b/testing/scripts/tests/dwi2response/fa_whitespace index 7b40b6b07f..c28d5a37c4 100644 --- a/testing/scripts/tests/dwi2response/fa_whitespace +++ b/testing/scripts/tests/dwi2response/fa_whitespace @@ -6,7 +6,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2response fa "tmp in.mif" "tmp out.txt" -force \ +dwi2response fa "tmp in.nii.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -voxels "tmp voxels.mif" \ -number 20 diff --git a/testing/scripts/tests/dwi2response/manual_piping b/testing/scripts/tests/dwi2response/manual_piping index 13f20e9913..2c2c34b376 100644 --- a/testing/scripts/tests/dwi2response/manual_piping +++ b/testing/scripts/tests/dwi2response/manual_piping @@ -3,7 +3,7 @@ # where image pipes are used # Input DWI is piped -mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_in.mif - \ +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -strides 0,0,0,1 \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwi2response manual - dwi2response/fa/default.mif.gz tmp.txt -force diff --git a/testing/scripts/tests/dwi2response/manual_whitespace b/testing/scripts/tests/dwi2response/manual_whitespace index a124963eb6..440148d740 100644 --- a/testing/scripts/tests/dwi2response/manual_whitespace +++ b/testing/scripts/tests/dwi2response/manual_whitespace @@ -12,7 +12,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s dwi2response/fa/default.mif.gz "tmp voxels.mif.gz" -dwi2response manual "tmp in.mif" "tmp voxels.mif.gz" "tmp out.txt" -force \ +dwi2response manual "tmp in.nii.gz" "tmp voxels.mif.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -dirs "tmp dirs.mif" diff --git a/testing/scripts/tests/dwi2response/msmt5tt_piping b/testing/scripts/tests/dwi2response/msmt5tt_piping index 808a1e39e6..8d37c1b46f 100644 --- a/testing/scripts/tests/dwi2response/msmt5tt_piping +++ b/testing/scripts/tests/dwi2response/msmt5tt_piping @@ -5,8 +5,7 @@ # Input DWI and output voxel selection image are piped mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -strides 0,0,0,1 \ --fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | - +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwi2response msmt_5tt - BIDS/sub-01/anat/sub-01_5TT.nii.gz \ tmp_wm.txt tmp_gm.txt tmp_csf.txt -force \ -voxels - \ @@ -17,6 +16,7 @@ testing_diff_image - dwi2response/msmt_5tt/default.mif.gz mrconvert BIDS/sub-01/anat/sub-01_5TT.nii.gz - | \ dwi2response msmt_5tt BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ tmp_wm.txt tmp_gm.txt tmp_csf.txt -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ -voxels tmp.mif \ -pvf 0.9 @@ -29,6 +29,7 @@ dwi2tensor BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ tensor2metric - -vector - | \ dwi2response msmt_5tt BIDS/sub-01/dwi/sub-01_dwi.nii.gz BIDS/sub-01/anat/sub-01_5TT.nii.gz \ tmp_wm.txt tmp_gm.txt tmp_csf.txt -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ -voxels tmp.mif \ -dirs - \ -pvf 0.9 diff --git a/testing/scripts/tests/dwi2response/msmt5tt_whitespace b/testing/scripts/tests/dwi2response/msmt5tt_whitespace index 32594275f4..acc7db6ef7 100644 --- a/testing/scripts/tests/dwi2response/msmt5tt_whitespace +++ b/testing/scripts/tests/dwi2response/msmt5tt_whitespace @@ -12,9 +12,9 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/anat/sub-01_5TT.nii.gz "tmp 5TT.nii.gz" -dwi2response msmt_5tt "tmp in.mif" "tmp 5TT.nii.gz" \ +dwi2response msmt_5tt "tmp in.nii.gz" "tmp 5TT.nii.gz" \ "tmp wm.txt" "tmp gm.txt" "tmp csf.txt" -force \ --fslgrad "tmp in.bvec" "tmp-in.bval" \ +-fslgrad "tmp in.bvec" "tmp in.bval" \ -dirs "tmp dirs.mif" \ -voxels "tmp voxels.mif" \ -pvf 0.9 diff --git a/testing/scripts/tests/dwi2response/tax_fslgrad b/testing/scripts/tests/dwi2response/tax_fslgrad index a04c9c2ff3..9f46e84160 100644 --- a/testing/scripts/tests/dwi2response/tax_fslgrad +++ b/testing/scripts/tests/dwi2response/tax_fslgrad @@ -5,9 +5,9 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tax tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ +dwi2response tax BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --force +-voxels tmp_voxels.mif testing_diff_matrix tmp_out.txt dwi2response/tax/default.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tax/default.mif.gz diff --git a/testing/scripts/tests/dwi2response/tax_grad b/testing/scripts/tests/dwi2response/tax_grad index 71092bc2af..07c4f6eae9 100644 --- a/testing/scripts/tests/dwi2response/tax_grad +++ b/testing/scripts/tests/dwi2response/tax_grad @@ -5,14 +5,13 @@ # Ensure that outputs match those generated # using a prior version of the software -mrinfo BIDS/sub-01/dwi/sub-01_dwi.nii.gz \ +mrinfo BIDS/sub-01/dwi/sub-01_dwi.nii.gz -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --export_grad_mrtrix tmp_grad.b \ --force +-export_grad_mrtrix tmp_grad.b -dwi2response tax tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ --grad tmp_grad.b \ --force +dwi2response tax BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ +-voxels tmp_voxels.mif \ +-grad tmp_grad.b testing_diff_matrix tmp_out.txt dwi2response/tax/default.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tax/default.mif.gz diff --git a/testing/scripts/tests/dwi2response/tax_lmax b/testing/scripts/tests/dwi2response/tax_lmax index 6a65bf82f2..4efe3a73e9 100644 --- a/testing/scripts/tests/dwi2response/tax_lmax +++ b/testing/scripts/tests/dwi2response/tax_lmax @@ -5,11 +5,10 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tax tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ +dwi2response tax BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --lmax 6 \ --force +-voxels tmp_voxels.mif \ +-lmax 6 testing_diff_matrix tmp_out.txt dwi2response/tax/lmax.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tax/lmax.mif.gz - diff --git a/testing/scripts/tests/dwi2response/tax_mask b/testing/scripts/tests/dwi2response/tax_mask index f9ec0d44a9..aee5a33cf3 100644 --- a/testing/scripts/tests/dwi2response/tax_mask +++ b/testing/scripts/tests/dwi2response/tax_mask @@ -4,10 +4,10 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tax tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ +dwi2response tax BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz \ --force +-voxels tmp_voxels.mif testing_diff_matrix tmp_out.txt dwi2response/tax/masked.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tax/masked.mif.gz diff --git a/testing/scripts/tests/dwi2response/tax_piping b/testing/scripts/tests/dwi2response/tax_piping index e796788022..663e34d663 100644 --- a/testing/scripts/tests/dwi2response/tax_piping +++ b/testing/scripts/tests/dwi2response/tax_piping @@ -2,7 +2,7 @@ # Verify successful execution of "dwi2response tax" # where image piping is used -mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_in.mif - \ +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -strides 0,0,0,1 \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwi2response tax - tmp.txt -force \ diff --git a/testing/scripts/tests/dwi2response/tax_shell b/testing/scripts/tests/dwi2response/tax_shell index 1f3ddb558b..f378b04cf6 100644 --- a/testing/scripts/tests/dwi2response/tax_shell +++ b/testing/scripts/tests/dwi2response/tax_shell @@ -5,10 +5,10 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tax tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ +dwi2response tax BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --shell 2000 \ --force +-voxels tmp_voxels.mif \ +-shell 2000 testing_diff_matrix tmp_out.txt dwi2response/tax/shell.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tax/shell.mif.gz diff --git a/testing/scripts/tests/dwi2response/tax_whitespace b/testing/scripts/tests/dwi2response/tax_whitespace index c49cd5fc41..f152ce4398 100644 --- a/testing/scripts/tests/dwi2response/tax_whitespace +++ b/testing/scripts/tests/dwi2response/tax_whitespace @@ -6,7 +6,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2response tax "tmp in.mif" "tmp out.txt" -force \ +dwi2response tax "tmp in.nii.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -voxels "tmp voxels.mif" diff --git a/testing/scripts/tests/dwi2response/tournier_fslgrad b/testing/scripts/tests/dwi2response/tournier_fslgrad index 8b03e13d65..a48d2f887c 100644 --- a/testing/scripts/tests/dwi2response/tournier_fslgrad +++ b/testing/scripts/tests/dwi2response/tournier_fslgrad @@ -5,11 +5,11 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tournier tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ --number 20 \ --iter_voxels 200 \ +dwi2response tournier BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --force +-voxels tmp_voxels.mif \ +-number 20 \ +-iter_voxels 200 testing_diff_matrix tmp_out.txt dwi2response/tournier/default.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tournier/default.mif.gz diff --git a/testing/scripts/tests/dwi2response/tournier_grad b/testing/scripts/tests/dwi2response/tournier_grad index 4baaf72303..17542f8cd6 100644 --- a/testing/scripts/tests/dwi2response/tournier_grad +++ b/testing/scripts/tests/dwi2response/tournier_grad @@ -5,16 +5,15 @@ # Ensure that outputs match those generated # using a prior version of the software -mrinfo BIDS/sub-01/dwi/sub-01_dwi.nii.gz \ +mrinfo BIDS/sub-01/dwi/sub-01_dwi.nii.gz -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --export_grad_mrtrix tmp_grad.b \ --force +-export_grad_mrtrix tmp_grad.b -dwi2response tournier tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ --number 20 \ --iter_voxels 200 \ +dwi2response tournier BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ -grad tmp_grad.b \ --force +-voxels tmp_voxels.mif \ +-number 20 \ +-iter_voxels 200 testing_diff_matrix tmp_out.txt dwi2response/tournier/default.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tournier/default.mif.gz diff --git a/testing/scripts/tests/dwi2response/tournier_lmax b/testing/scripts/tests/dwi2response/tournier_lmax index 76ffb50326..d57bce92bd 100644 --- a/testing/scripts/tests/dwi2response/tournier_lmax +++ b/testing/scripts/tests/dwi2response/tournier_lmax @@ -5,12 +5,12 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tournier tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ +dwi2response tournier BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-voxels tmp_voxels.mif \ -number 20 \ -iter_voxels 200 \ --fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --lmax 6 \ --force +-lmax 6 testing_diff_matrix tmp_out.txt dwi2response/tournier/lmax.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tournier/lmax.mif.gz diff --git a/testing/scripts/tests/dwi2response/tournier_mask b/testing/scripts/tests/dwi2response/tournier_mask index 4145215350..180c54f655 100644 --- a/testing/scripts/tests/dwi2response/tournier_mask +++ b/testing/scripts/tests/dwi2response/tournier_mask @@ -4,12 +4,12 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tournier tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ --number 20 \ --iter_voxels 200 \ +dwi2response tournier BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz \ --force +-voxels tmp_voxels.mif \ +-number 20 \ +-iter_voxels 200 testing_diff_matrix tmp_out.txt dwi2response/tournier/masked.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tournier/masked.mif.gz diff --git a/testing/scripts/tests/dwi2response/tournier_shell b/testing/scripts/tests/dwi2response/tournier_shell index 011f7fb18b..bd6e92245c 100644 --- a/testing/scripts/tests/dwi2response/tournier_shell +++ b/testing/scripts/tests/dwi2response/tournier_shell @@ -6,12 +6,12 @@ # Ensure that outputs match those generated # using a prior version of the software -dwi2response tournier tmp_in.mif tmp_out.txt -voxels tmp_voxels.mif \ +dwi2response tournier BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmp_out.txt -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ +-voxels tmp_voxels.mif \ -number 20 \ -iter_voxels 200 \ --fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ --shell 2000 \ --force +-shell 2000 testing_diff_matrix tmp_out.txt dwi2response/tournier/shell.txt -abs 1e-2 testing_diff_image tmp_voxels.mif dwi2response/tournier/shell.mif.gz diff --git a/testing/scripts/tests/dwi2response/tournier_whitespace b/testing/scripts/tests/dwi2response/tournier_whitespace index 5cc26ca3c7..fe886ccefa 100644 --- a/testing/scripts/tests/dwi2response/tournier_whitespace +++ b/testing/scripts/tests/dwi2response/tournier_whitespace @@ -6,7 +6,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwi2response tournier "tmp in.mif" "tmp out.txt" -force \ +dwi2response tournier "tmp in.nii.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -voxels "tmp voxels.mif" \ -number 20 \ diff --git a/testing/scripts/tests/dwibiascorrect/ants_whitespace b/testing/scripts/tests/dwibiascorrect/ants_whitespace index 8edf806526..2f33d38e87 100644 --- a/testing/scripts/tests/dwibiascorrect/ants_whitespace +++ b/testing/scripts/tests/dwibiascorrect/ants_whitespace @@ -7,7 +7,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" -dwibiascorrect ants "tmp in.mif" "tmp out.mif" -force \ +dwibiascorrect ants "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ -bias "tmp bias.mif" diff --git a/testing/scripts/tests/dwibiascorrect/fsl_whitespace b/testing/scripts/tests/dwibiascorrect/fsl_whitespace index bdfa5e5860..105ac48a2e 100644 --- a/testing/scripts/tests/dwibiascorrect/fsl_whitespace +++ b/testing/scripts/tests/dwibiascorrect/fsl_whitespace @@ -8,10 +8,10 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" -dwibiascorrect fsl "tmp in.mif" "tmp out.mif" \ +dwibiascorrect fsl "tmp in.nii.gz" "tmp out.mif" \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ -bias "tmp bias.mif" testing_diff_header "tmp out.mif" dwibiascorrect/fsl/default_out.mif.gz -testing_diff_header "tmp bias" dwibiascorrect/fsl/default_bias.mif.gz +testing_diff_header "tmp bias.mif" dwibiascorrect/fsl/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect/mtnorm_lmax b/testing/scripts/tests/dwibiascorrect/mtnorm_lmax index 9e3826257d..6cf101c975 100644 --- a/testing/scripts/tests/dwibiascorrect/mtnorm_lmax +++ b/testing/scripts/tests/dwibiascorrect/mtnorm_lmax @@ -4,7 +4,7 @@ dwibiascorrect mtnorm BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmpout.mif -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ -lmax 6,0,0 \ --bias ../tmp/dwibiascorrect/mtnorm/lmax600_bias.mif +-bias tmpbias.mif testing_diff_image tmpout.mif dwibiascorrect/mtnorm/lmax600_out.mif.gz testing_diff_image tmpbias.mif dwibiascorrect/mtnorm/lmax600_bias.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace b/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace index f9b32860ec..fb3e0644a6 100644 --- a/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace +++ b/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace @@ -7,7 +7,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" -dwibiascorrect mtnorm "tmp in.mif" "tmp out.mif" \ +dwibiascorrect mtnorm "tmp in.nii.gz" "tmp out.mif" \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ -bias "tmp bias.mif" diff --git a/testing/scripts/tests/dwibiasnormmask/piping b/testing/scripts/tests/dwibiasnormmask/piping index e9e39e55dc..35237680ab 100644 --- a/testing/scripts/tests/dwibiasnormmask/piping +++ b/testing/scripts/tests/dwibiasnormmask/piping @@ -5,7 +5,7 @@ # Input DWI series, and output DWI series, are pipes mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ -dwibiasnormmask - - | \ +dwibiasnormmask - - tmp_mask.mif -force | \ testing_diff_image - dwibiasnormmask/default_out.mif.gz -frac 1e-5 # Prepare some input data to be subsequently used in multiple tests @@ -25,9 +25,9 @@ testing_diff_image - dwibiasnormmask/default_mask.mif.gz # Output bias field image is a pipe dwibiasnormmask tmp_in.mif tmp_out.mif tmp_mask.mif -force \ -output_bias - | \ -testing_diff_image tmpbias.mif dwibiasnormmask/default_bias.mif.gz -frac 1e-5 +testing_diff_image - dwibiasnormmask/default_bias.mif.gz -frac 1e-5 # Output tissue sum image is a pipe dwibiasnormmask tmp_in.mif tmp_out.mif tmp_mask.mif -force \ -output_tissuesum - | \ -testing_diff_image tmptissuesum.mif dwibiasnormmask/default_tissuesum.mif.gz -abs 1e-5 +testing_diff_image - dwibiasnormmask/default_tissuesum.mif.gz -abs 1e-5 diff --git a/testing/scripts/tests/dwibiasnormmask/reference b/testing/scripts/tests/dwibiasnormmask/reference index cd7b5bab41..d718046f2d 100644 --- a/testing/scripts/tests/dwibiasnormmask/reference +++ b/testing/scripts/tests/dwibiasnormmask/reference @@ -6,7 +6,8 @@ # Therefore, if a different reference intensity is requested, # it should be possible to multiply the result of such by the ratio of those references # and that result should then match the pre-calculated one -dwibiasnormmask tmp-sub-01_dwi.mif tmpout.mif tmpmask.mif -force \ +dwibiasnormmask BIDS/sub-01/dwi/sub-01_dwi.nii.gz tmpout.mif tmpmask.mif -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ -reference 1.0 \ -output_scale tmpscale.txt diff --git a/testing/scripts/tests/dwibiasnormmask/whitespace b/testing/scripts/tests/dwibiasnormmask/whitespace index fb98b16833..aac1db8b5f 100644 --- a/testing/scripts/tests/dwibiasnormmask/whitespace +++ b/testing/scripts/tests/dwibiasnormmask/whitespace @@ -8,7 +8,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" -dwibiasnormmask "tmp in.mif" "tmp out.mif" -force \ +dwibiasnormmask "tmp in.nii.gz" "tmp out.mif" "tmp mask.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -init_mask "tmp mask.nii.gz" \ -output_bias "tmp bias.mif" \ @@ -16,6 +16,7 @@ dwibiasnormmask "tmp in.mif" "tmp out.mif" -force \ -output_tissuesum "tmp tissuesum.mif" ls | grep "^tmp out\.mif$" +ls | grep "^tmp mask.mif$" ls | grep "^tmp bias\.mif$" ls | grep "^tmp scale\.txt$" ls | grep "^tmp tissuesum\.mif$" diff --git a/testing/scripts/tests/dwicat/piping b/testing/scripts/tests/dwicat/piping index cc00bdd40a..9b7d5cf1ff 100644 --- a/testing/scripts/tests/dwicat/piping +++ b/testing/scripts/tests/dwicat/piping @@ -11,24 +11,25 @@ dwiextract tmp.mif tmp01_b3000.mif -shells 0,3000 -force mrcat tmp01_b1000.mif tmp01_b2000.mif tmp01_b3000.mif -axis 3 tmp02.mif -force # Set of series where intensity between shells is artificially modulated +mrcalc tmp01_b1000.mif 1.0 -mult tmp03_b1000.mif -force mrcalc tmp01_b2000.mif 0.2 -mult tmp03_b2000.mif -force mrcalc tmp01_b3000.mif 5.0 -mult tmp03_b3000.mif -force mrcat tmp01_b1000.mif tmp03_b2000.mif tmp03_b3000.mif -axis 3 tmp03.mif -force # Provide one of the input images as a pipe -mrconvert tmp01_b1000.mif - | +mrconvert tmp03_b1000.mif - | \ dwicat - tmp03_b2000.mif tmp03_b3000.mif tmp.mif -force \ -mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz testing_diff_image tmp.mif tmp02.mif -frac 1e-6 # Provide the brain mask as a pipe -mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | +mrconvert BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | \ dwicat tmp03_b1000.mif tmp03_b2000.mif tmp03_b3000.mif tmp.mif -force \ -mask - testing_diff_image tmp.mif tmp02.mif -frac 1e-6 # Export the output image as a pipe dwicat tmp03_b1000.mif tmp03_b2000.mif tmp03_b3000.mif - \ --mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz \ +-mask BIDS/sub-01/dwi/sub-01_brainmask.nii.gz | \ testing_diff_image - tmp02.mif -frac 1e-6 diff --git a/testing/scripts/tests/dwicat/whitespace b/testing/scripts/tests/dwicat/whitespace index 34c11a94aa..ffe1f28e22 100644 --- a/testing/scripts/tests/dwicat/whitespace +++ b/testing/scripts/tests/dwicat/whitespace @@ -3,13 +3,12 @@ # where image paths include whitespace characters rm -f "tmp out.mif" -ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" -ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" -ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.mif" -force \ +-fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval + ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" dwicat "tmp in.mif" "tmp in.mif" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" \ --mask "tmp mask.mif" +-mask "tmp mask.nii.gz" ls | grep "tmp out.mif" diff --git a/testing/scripts/tests/dwifslpreproc/piping b/testing/scripts/tests/dwifslpreproc/piping index 221724dd60..08acec1d4e 100644 --- a/testing/scripts/tests/dwifslpreproc/piping +++ b/testing/scripts/tests/dwifslpreproc/piping @@ -18,6 +18,7 @@ mrconvert BIDS/sub-04/fmap/sub-04_dir-2_epi.nii.gz tmp2.mif -force \ -json_import BIDS/sub-04/fmap/sub-04_dir-2_epi.json mrcat tmp1.mif tmp2.mif -axis 3 - | \ dwifslpreproc BIDS/sub-04/dwi/sub-04_dwi.nii.gz tmp.mif -force \ +-fslgrad BIDS/sub-04/dwi/sub-04_dwi.bvec BIDS/sub-04/dwi/sub-04_dwi.bval \ -pe_dir ap -readout_time 0.1 -rpe_pair \ -se_epi - testing_diff_header tmp.mif dwifslpreproc/rpepair_default.mif.gz diff --git a/testing/scripts/tests/dwifslpreproc/rpenone_default b/testing/scripts/tests/dwifslpreproc/rpenone_default index a9d6ac8bd8..8b6c70a83a 100644 --- a/testing/scripts/tests/dwifslpreproc/rpenone_default +++ b/testing/scripts/tests/dwifslpreproc/rpenone_default @@ -17,7 +17,7 @@ dwifslpreproc tmp-sub-04_dwi.mif tmp.mif -force \ testing_diff_header tmp.mif dwifslpreproc/rpenone_default.mif.gz dwifslpreproc BIDS/sub-04/dwi/sub-04_dwi.nii.gz tmp.mif -force \ --fslgrad BIDS/sub-04/dwi/sub-04_dwi/bvec BIDS/sub-04/dwi/sub-04_dwi.bval \ +-fslgrad BIDS/sub-04/dwi/sub-04_dwi.bvec BIDS/sub-04/dwi/sub-04_dwi.bval \ -pe_dir ap -readout_time 0.1 -rpe_none testing_diff_header tmp.mif dwifslpreproc/rpenone_default.mif.gz diff --git a/testing/scripts/tests/dwifslpreproc/whitespace b/testing/scripts/tests/dwifslpreproc/whitespace index d0db8f2f59..eb3c00cec8 100644 --- a/testing/scripts/tests/dwifslpreproc/whitespace +++ b/testing/scripts/tests/dwifslpreproc/whitespace @@ -14,19 +14,20 @@ mrconvert BIDS/sub-04/fmap/sub-04_dir-2_epi.nii.gz tmp2.mif -force \ -json_import BIDS/sub-04/fmap/sub-04_dir-2_epi.json mrcat tmp1.mif tmp2.mif "tmp seepi.mif" -axis 3 -force -dwi2mask BIDS/sub-04/dwi/sub-04_dwi.nii.gz tmpeddymask.mif -force \ +dwi2mask legacy BIDS/sub-04/dwi/sub-04_dwi.nii.gz tmpeddymask.mif -force \ -fslgrad BIDS/sub-04/dwi/sub-04_dwi.bvec BIDS/sub-04/dwi/sub-04_dwi.bval ln -s tmpeddymask.mif "tmp eddymask.mif" dwifslpreproc "tmp in.nii.gz" "tmp out.nii" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -json_import "tmp in.json" \ --pe_dir ap -readout_time 0.1 -rpe_pair "tmp seepi.mif" \ +-pe_dir ap -readout_time 0.1 -rpe_pair \ +-se_epi "tmp seepi.mif" \ -eddy_mask "tmp eddymask.mif" \ --eddyqc_test "tmp eddyqc/" \ +-eddyqc_text "tmp eddyqc" \ -export_grad_fsl "tmp out.bvec" "tmp out.bval" ls | grep "^tmp out\.nii$" ls | grep "^tmp out\.bvec$" ls | grep "^tmp out\.bval$" -ls | grep "^tmp eddyqc\/$" +ls | grep "^tmp eddyqc$" diff --git a/testing/scripts/tests/dwigradcheck/whitespace b/testing/scripts/tests/dwigradcheck/whitespace index 2bae2b044c..a0c60e0285 100644 --- a/testing/scripts/tests/dwigradcheck/whitespace +++ b/testing/scripts/tests/dwigradcheck/whitespace @@ -9,7 +9,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwigradcheck "tmp in.mif" \ +dwigradcheck "tmp in.nii.gz" \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ -export_grad_fsl "tmp out.bvec" "tmp out.bval" diff --git a/testing/scripts/tests/dwinormalise/group_default b/testing/scripts/tests/dwinormalise/group_default index 7f59d90704..a855f7111f 100644 --- a/testing/scripts/tests/dwinormalise/group_default +++ b/testing/scripts/tests/dwinormalise/group_default @@ -10,7 +10,7 @@ mrconvert BIDS/sub-02/dwi/sub-02_brainmask.nii.gz tmp-mask/sub-02.mif mrconvert BIDS/sub-03/dwi/sub-03_dwi.nii.gz tmp-dwi/sub-03.mif \ -fslgrad BIDS/sub-03/dwi/sub-03_dwi.bvec BIDS/sub-03/dwi/sub-03_dwi.bval -mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz tmp-mask/sub-03.mif +mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz tmp-mask/sub-03.mif dwinormalise group tmp-dwi/ tmp-mask/ tmp-group/ tmp-fa.mif tmp-mask.mif -force diff --git a/testing/scripts/tests/dwinormalise/manual_piping b/testing/scripts/tests/dwinormalise/manual_piping index 1cd9c93f18..d50b901a9c 100644 --- a/testing/scripts/tests/dwinormalise/manual_piping +++ b/testing/scripts/tests/dwinormalise/manual_piping @@ -3,7 +3,7 @@ # where image pipes are used # Input and output DWI series are both piped -mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - | +mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval | \ dwinormalise manual - BIDS/sub-01/dwi/sub-01_brainmask.nii.gz - | \ testing_diff_image - dwinormalise/manual/out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwinormalise/manual_whitespace b/testing/scripts/tests/dwinormalise/manual_whitespace index 2e538af06f..2e6026dd3e 100644 --- a/testing/scripts/tests/dwinormalise/manual_whitespace +++ b/testing/scripts/tests/dwinormalise/manual_whitespace @@ -3,12 +3,12 @@ # where image paths include whitespace characters # Input and output DWI series are both piped -ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.mif" +ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" -dwinormalise manual "tmp in.mif" "tmp mask.nii.gz" "tmp out.mif" -force \ +dwinormalise manual "tmp in.nii.gz" "tmp mask.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" testing_diff_image "tmp out.mif" dwinormalise/manual/out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwinormalise/mtnorm_whitespace b/testing/scripts/tests/dwinormalise/mtnorm_whitespace index b50bd8a140..63470f542f 100644 --- a/testing/scripts/tests/dwinormalise/mtnorm_whitespace +++ b/testing/scripts/tests/dwinormalise/mtnorm_whitespace @@ -7,7 +7,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" -dwinormalise mtnorm "tmp in.mif" "tmp out.mif" -force \ +dwinormalise mtnorm "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" diff --git a/testing/scripts/tests/dwishellmath/whitespace b/testing/scripts/tests/dwishellmath/whitespace index f30be48601..81012ff6a3 100644 --- a/testing/scripts/tests/dwishellmath/whitespace +++ b/testing/scripts/tests/dwishellmath/whitespace @@ -6,7 +6,7 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" -dwishellmath "tmp in.mif" mean "tmp out.mif" -force \ +dwishellmath "tmp in.nii.gz" mean "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/labelsgmfirst/piping b/testing/scripts/tests/labelsgmfirst/piping index eed573ace4..e6945ee023 100644 --- a/testing/scripts/tests/labelsgmfirst/piping +++ b/testing/scripts/tests/labelsgmfirst/piping @@ -8,6 +8,6 @@ labelsgmfirst - BIDS/sub-01/anat/sub-01_T1w.nii.gz BIDS/parc-desikan_lookup.txt testing_diff_header - labelsgmfirst/default.mif.gz # Input T1-weighted image is a pipe -mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz - | +mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz - | \ labelsgmfirst BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz - BIDS/parc-desikan_lookup.txt tmp.mif -force -testig_diff_header tmp.mif labelsgmfirst/default.mif.gz +testing_diff_header tmp.mif labelsgmfirst/default.mif.gz From 92f9a89a341e7d85b2c804e1dd135deb05ae01e9 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 16 May 2024 14:04:40 +1000 Subject: [PATCH 67/75] 5ttgen hsvs: Fixes to whitespace handling changes --- python/lib/mrtrix3/_5ttgen/hsvs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lib/mrtrix3/_5ttgen/hsvs.py b/python/lib/mrtrix3/_5ttgen/hsvs.py index c1464c4ab5..d71eddf8ee 100644 --- a/python/lib/mrtrix3/_5ttgen/hsvs.py +++ b/python/lib/mrtrix3/_5ttgen/hsvs.py @@ -459,7 +459,7 @@ def execute(): #pylint: disable=unused-variable bs_fullmask_path = 'brain_stem_init.mif' bs_cropmask_path = '' progress = app.ProgressBar('Segmenting and cropping brain stem', 5) - cmd = ['mrcalc', aparc_image, BRAIN_STEM_ASEG[0][0], '-eq'] + cmd = ['mrcalc', aparc_image, f'{BRAIN_STEM_ASEG[0][0]}', '-eq'] for index, name in BRAIN_STEM_ASEG[1:]: cmd.extend([aparc_image, f'{index}', '-eq', '-add']) cmd.extend([bs_fullmask_path, '-datatype', 'bit']) @@ -749,7 +749,7 @@ def voxel2scanner(voxel, header): smooth_mesh_path = f'{hemi}-Cerebellum-All-Smooth.vtk' pvf_image_path = f'{hemi}-Cerebellum-PVF-Template.mif' cerebellum_aseg_hemi = [ entry for entry in CEREBELLUM_ASEG if hemi in entry[2] ] - cmd = ['mrcalc', aparc_image, cerebellum_aseg_hemi[0][0], '-eq'] + cmd = ['mrcalc', aparc_image, f'{cerebellum_aseg_hemi[0][0]}', '-eq'] for index, tissue, name in cerebellum_aseg_hemi[1:]: cmd.extend([aparc_image, f'{index}', '-eq', '-add']) cmd.extend(['-', '|', @@ -779,7 +779,7 @@ def voxel2scanner(voxel, header): else: app.console('Preparing images of cerebellum for intensity-based segmentation') - cmd = ['mrcalc', aparc_image, CEREBELLUM_ASEG[0][0], '-eq'] + cmd = ['mrcalc', aparc_image, f'{CEREBELLUM_ASEG[0][0]}', '-eq'] for index, tissue, name in CEREBELLUM_ASEG[1:]: cmd.extend([aparc_image, f'{index}', '-eq', '-add']) cmd.append(cerebellum_volume_image) From fc4b79800bb210ce343ee4aea2e86e3f30d49618 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 17 May 2024 09:56:49 +1000 Subject: [PATCH 68/75] population_template: Fix handling of output directory paths --- python/bin/population_template | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/python/bin/population_template b/python/bin/population_template index 5554f86efa..1f2fbefe8a 100755 --- a/python/bin/population_template +++ b/python/bin/population_template @@ -309,11 +309,6 @@ def abspath(arg, *args): return os.path.abspath(os.path.join(arg, *args)) -def relpath(arg, *args): - from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - return os.path.relpath(os.path.join(arg, *args), app.WORKING_DIR) - - def copy(src, dst, follow_symlinks=True): """Copy data but do not set mode bits. Return the file's destination. @@ -934,9 +929,6 @@ def execute(): #pylint: disable=unused-variable raise MRtrixError(f'mismatch between number of output templates ({len(app.ARGS.template)}) ' 'and number of contrasts ({n_contrasts})') - if app.ARGS.warp_dir: - app.ARGS.warp_dir = relpath(app.ARGS.warp_dir) - if app.ARGS.transformed_dir: if len(app.ARGS.transformed_dir) != n_contrasts: raise MRtrixError(f'number of output directories specified for transformed images ({len(app.ARGS.transformed_dir)})' @@ -947,7 +939,6 @@ def execute(): #pylint: disable=unused-variable if app.ARGS.linear_transformations_dir: if not dolinear: raise MRtrixError("linear option set when no linear registration is performed") - app.ARGS.linear_transformations_dir = relpath(app.ARGS.linear_transformations_dir) # automatically detect SH series in each contrast do_fod_registration = False # in any contrast @@ -1700,11 +1691,8 @@ def execute(): #pylint: disable=unused-variable force=app.FORCE_OVERWRITE) if app.ARGS.warp_dir: - warp_path = app.ARGS.warp_dir - if os.path.exists(warp_path): - run.function(shutil.rmtree, warp_path) - os.makedirs(warp_path) - progress = app.ProgressBar(f'Copying non-linear warps to output directory "{warp_path}"', len(ins)) + app.ARGS.warp_dir.mkdir() + progress = app.ProgressBar(f'Copying non-linear warps to output directory "{app.ARGS.warp_dir}"', len(ins)) for inp in ins: keyval = image.Header(os.path.join('warps', f'{inp.uid}.mif')).keyval() keyval = dict((k, keyval[k]) for k in ('linear1', 'linear2')) @@ -1713,20 +1701,17 @@ def execute(): #pylint: disable=unused-variable json.dump(keyval, json_file) run.command(['mrconvert', os.path.join('warps', f'{inp.uid}.mif'), - os.path.join(warp_path, f'{xcontrast_xsubject_pre_postfix[0]}{inp.uid}{xcontrast_xsubject_pre_postfix[1]}.mif')], + os.path.join(app.ARGS.warp_dir, f'{xcontrast_xsubject_pre_postfix[0]}{inp.uid}{xcontrast_xsubject_pre_postfix[1]}.mif')], mrconvert_keyval=json_path, force=app.FORCE_OVERWRITE) progress.increment() progress.done() if app.ARGS.linear_transformations_dir: - linear_transformations_path = app.ARGS.linear_transformations_dir - if os.path.exists(linear_transformations_path): - run.function(shutil.rmtree, linear_transformations_path) - os.makedirs(linear_transformations_path) + app.ARGS.linear_transformations_dir.mkdir() for inp in ins: trafo = matrix.load_transform(os.path.join('linear_transforms', f'{inp.uid}.txt')) - matrix.save_transform(os.path.join(linear_transformations_path, + matrix.save_transform(os.path.join(app.ARGS.linear_transformations_dir, f'{xcontrast_xsubject_pre_postfix[0]}{inp.uid}{xcontrast_xsubject_pre_postfix[1]}.txt'), trafo, force=app.FORCE_OVERWRITE) From f34730d5e35cfbd0dc60db6467d3d9e96ebb6765 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 17 May 2024 09:58:08 +1000 Subject: [PATCH 69/75] dwi2mask consensus: Fix for whitespace in scratch directory path --- python/lib/mrtrix3/dwi2mask/consensus.py | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/python/lib/mrtrix3/dwi2mask/consensus.py b/python/lib/mrtrix3/dwi2mask/consensus.py index 228367bfc3..c585cf67d3 100644 --- a/python/lib/mrtrix3/dwi2mask/consensus.py +++ b/python/lib/mrtrix3/dwi2mask/consensus.py @@ -75,9 +75,10 @@ def execute(): #pylint: disable=unused-variable # Don't use "-software antsquick"; we're assuming that "antsfull" is superior if 'b02template' in algorithm_list: algorithm_list = [item for item in algorithm_list if item != 'b02template'] - algorithm_list.append('b02template -software antsfull') - algorithm_list.append('b02template -software fsl') + algorithm_list.append(['b02template', '-software', 'antsfull']) + algorithm_list.append(['b02template', '-software', 'fsl']) + # Are we using at least one algorithm that necessitates a template image & mask? if any(any(item in alg for item in ('ants', 'b02template')) for alg in algorithm_list): if app.ARGS.template: run.command(['mrconvert', app.ARGS.template[0], 'template_image.nii', @@ -91,31 +92,37 @@ def execute(): #pylint: disable=unused-variable '-strides', '+1,+2,+3', '-datatype', 'uint8']) elif app.ARGS.algorithms: raise MRtrixError('Cannot include within consensus algorithms that necessitate use of a template image ' - 'if no template image is provided via command-line or configuration file') + 'if no template image is provided via command-line or configuration file') else: app.warn('No template image and mask provided;' ' algorithms based on registration to template will be excluded from consensus') - algorithm_list = [item for item in algorithm_list if (item != 'ants' and not item.startswith('b02template'))] + algorithm_list = [item for item in algorithm_list if (item != 'ants' and not 'b02template' in item)] app.debug(str(algorithm_list)) mask_list = [] for alg in algorithm_list: - alg_string = alg.replace(' -software ', '_') + cmd = ['dwi2mask'] + if isinstance(alg, list): + cmd.extend(alg) + alg_string = f'{alg[0]}_{alg[-1]}' + else: + cmd.append(alg) + alg_string = alg mask_path = f'{alg_string}.mif' - cmd = f'dwi2mask {alg} input.mif {mask_path}' + cmd.extend(['input.mif', mask_path]) # Ideally this would be determined based on the presence of this option # in the command's help page if any(item in alg for item in ['ants', 'b02template']): - cmd += ' -template template_image.nii template_mask.nii' - cmd += f' -scratch {app.SCRATCH_DIR}' + cmd.extend(['-template', 'template_image.nii', 'template_mask.nii']) + cmd.extend(['-scratch', app.SCRATCH_DIR]) if not app.DO_CLEANUP: - cmd += ' -nocleanup' + cmd.append('-nocleanup') try: run.command(cmd) mask_list.append(mask_path) except run.MRtrixCmdError as e_dwi2mask: - app.warn('"dwi2mask ' + alg + '" failed; will be omitted from consensus') + app.warn('"dwi2mask ' + alg_string + '" failed; will be omitted from consensus') app.debug(str(e_dwi2mask)) with open(f'error_{alg_string}.txt', 'w', encoding='utf-8') as f_error: f_error.write(str(e_dwi2mask) + '\n') From 2a55a43df670d4cb0505e30d6ddd0c866f65fb25 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 17 May 2024 10:00:17 +1000 Subject: [PATCH 70/75] Testing: Whitespaces in scratch directory paths --- python/lib/mrtrix3/app.py | 2 +- testing/scripts/tests/5ttgen/freesurfer_whitespace | 8 +++++++- testing/scripts/tests/5ttgen/fsl_whitespace | 6 +++++- testing/scripts/tests/5ttgen/hsvs_whitespace | 8 +++++++- testing/scripts/tests/dwi2mask/3dautomask_whitespace | 5 ++++- testing/scripts/tests/dwi2mask/ants_whitespace | 6 +++++- testing/scripts/tests/dwi2mask/b02template_whitespace | 6 +++++- testing/scripts/tests/dwi2mask/consensus_whitespace | 6 +++++- testing/scripts/tests/dwi2mask/fslbet_whitespace | 6 +++++- testing/scripts/tests/dwi2mask/hdbet_whitespace | 6 +++++- testing/scripts/tests/dwi2mask/legacy_whitespace | 7 ++++++- testing/scripts/tests/dwi2mask/mean_whitespace | 7 ++++++- testing/scripts/tests/dwi2mask/mtnorm_whitespace | 6 +++++- testing/scripts/tests/dwi2mask/synthstrip_whitespace | 6 +++++- testing/scripts/tests/dwi2mask/trace_whitespace | 7 ++++++- testing/scripts/tests/dwi2response/dhollander_whitespace | 6 +++++- testing/scripts/tests/dwi2response/fa_whitespace | 6 +++++- testing/scripts/tests/dwi2response/manual_whitespace | 6 +++++- testing/scripts/tests/dwi2response/msmt5tt_whitespace | 6 +++++- testing/scripts/tests/dwi2response/tax_whitespace | 6 +++++- testing/scripts/tests/dwi2response/tournier_whitespace | 6 +++++- testing/scripts/tests/dwibiascorrect/ants_whitespace | 6 +++++- testing/scripts/tests/dwibiascorrect/fsl_whitespace | 7 +++++-- testing/scripts/tests/dwibiascorrect/mtnorm_whitespace | 6 +++++- testing/scripts/tests/dwibiasnormmask/whitespace | 6 +++++- testing/scripts/tests/dwicat/whitespace | 6 +++++- testing/scripts/tests/dwifslpreproc/whitespace | 5 ++++- testing/scripts/tests/dwigradcheck/whitespace | 6 +++++- testing/scripts/tests/dwinormalise/group_whitespace | 7 +++++-- testing/scripts/tests/dwinormalise/manual_whitespace | 7 +++++-- testing/scripts/tests/dwinormalise/mtnorm_whitespace | 6 +++++- testing/scripts/tests/dwishellmath/whitespace | 6 +++++- testing/scripts/tests/labelsgmfirst/whitespace | 6 +++++- testing/scripts/tests/mask2glass/whitespace | 6 +++++- testing/scripts/tests/population_template/whitespace | 5 +++-- testing/scripts/tests/responsemean/whitespace | 2 +- 36 files changed, 175 insertions(+), 40 deletions(-) diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 0c6a4f4055..25dde69d45 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -328,7 +328,7 @@ def activate_scratch_dir(): #pylint: disable=unused-variable if SCRATCH_DIR: raise Exception('Cannot use multiple scratch directories') if hasattr(ARGS, 'scratch') and ARGS.scratch: - dir_path = os.path.abspath(ARGS.scratch) + dir_path = ARGS.scratch else: # Defaulting to working directory since too many users have encountered storage issues dir_path = CONFIG.get('ScriptScratchDir', WORKING_DIR) diff --git a/testing/scripts/tests/5ttgen/freesurfer_whitespace b/testing/scripts/tests/5ttgen/freesurfer_whitespace index 1610756b6a..83fc34ff74 100644 --- a/testing/scripts/tests/5ttgen/freesurfer_whitespace +++ b/testing/scripts/tests/5ttgen/freesurfer_whitespace @@ -1,6 +1,12 @@ #!/bin/bash # Ensure correct operation of the "5ttgen freesurfer" command # where image paths include whitespace +rm -rf "tmp scratch" +mkdir "tmp scratch" + mrconvert BIDS/sub-01/anat/aparc+aseg.mgz "tmp in.mif" -force -5ttgen freesurfer "tmp in.mif" "tmp out.mif" -force + +5ttgen freesurfer "tmp in.mif" "tmp out.mif" -force \ +-scratch "tmp scratch" + testing_diff_image "tmp out.mif" 5ttgen/freesurfer/default.mif.gz diff --git a/testing/scripts/tests/5ttgen/fsl_whitespace b/testing/scripts/tests/5ttgen/fsl_whitespace index 64db02c8b4..23999f4a5b 100644 --- a/testing/scripts/tests/5ttgen/fsl_whitespace +++ b/testing/scripts/tests/5ttgen/fsl_whitespace @@ -3,9 +3,13 @@ # where image paths include whitespace # Make sure that the output image is stored at the expected path rm -f "tmp out.mif" +rm -rf "tmp scratch" + +mkdir "tmp scratch" mrconvert BIDS/sub-01/anat/sub-01_T1w.nii.gz "tmp in.mif" -force -5ttgen fsl "tmp in.mif" "tmp out.mif" -force +5ttgen fsl "tmp in.mif" "tmp out.mif" -force \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/5ttgen/hsvs_whitespace b/testing/scripts/tests/5ttgen/hsvs_whitespace index 0b5122ca45..e1285d4c99 100644 --- a/testing/scripts/tests/5ttgen/hsvs_whitespace +++ b/testing/scripts/tests/5ttgen/hsvs_whitespace @@ -1,6 +1,12 @@ #!/bin/bash # Ensure correct operation of the "5ttgen hsvs" command # when filesystem paths include whitespace characters +rm -rf "tmp scratch" + ln -s freesurfer/sub-01 "tmp in" -5ttgen hsvs "tmp in" "tmp out.mif" -force +mkdir "tmp scratch" + +5ttgen hsvs "tmp in" "tmp out.mif" -force \ +-scratch "tmp scratch" + testing_diff_header "tmp out.mif" 5ttgen/hsvs/default.mif.gz diff --git a/testing/scripts/tests/dwi2mask/3dautomask_whitespace b/testing/scripts/tests/dwi2mask/3dautomask_whitespace index 900de181f4..c866beebba 100644 --- a/testing/scripts/tests/dwi2mask/3dautomask_whitespace +++ b/testing/scripts/tests/dwi2mask/3dautomask_whitespace @@ -3,12 +3,15 @@ # where image paths include whitespace characters # Make sure that the output image appears at the expected location rm -f "tmp in.mif" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" dwi2mask 3dautomask "tmp in.nii.gz" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/ants_whitespace b/testing/scripts/tests/dwi2mask/ants_whitespace index 10b97f16dc..9b827f6525 100644 --- a/testing/scripts/tests/dwi2mask/ants_whitespace +++ b/testing/scripts/tests/dwi2mask/ants_whitespace @@ -3,6 +3,7 @@ # when utilising image paths that include whitespace characters # Make sure that the output image appears at the expected location rm -f "tmp out.mif" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" @@ -11,8 +12,11 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" ln -s dwi2mask/template_mask.mif.gz "tmp mask.mif.gz" +mkdir "tmp scratch" + dwi2mask ants "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ --template "tmp template.mif.gz" "tmp mask.mif.gz" +-template "tmp template.mif.gz" "tmp mask.mif.gz" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/b02template_whitespace b/testing/scripts/tests/dwi2mask/b02template_whitespace index 32039bac82..66ed1bf1e8 100644 --- a/testing/scripts/tests/dwi2mask/b02template_whitespace +++ b/testing/scripts/tests/dwi2mask/b02template_whitespace @@ -3,6 +3,7 @@ # when image paths include whitespace characters # Ensure that the output image appears at the expected location rm -f "tmp out.mif" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" @@ -11,9 +12,12 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" ln -s dwi2mask/template_mask.mif.gz "tmp mask.mif.gz" +mkdir "tmp scratch" + dwi2mask b02template "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -software antsquick \ --template "tmp template.mif.gz" "tmp mask.mif.gz" +-template "tmp template.mif.gz" "tmp mask.mif.gz" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/consensus_whitespace b/testing/scripts/tests/dwi2mask/consensus_whitespace index e04994e903..96ab665f60 100644 --- a/testing/scripts/tests/dwi2mask/consensus_whitespace +++ b/testing/scripts/tests/dwi2mask/consensus_whitespace @@ -3,6 +3,7 @@ # when utilising image paths that include whitespace characters # Make sure that output image appears at the expected location rm -f "tmp out.mif" +rm -rf "tmp scratch" ln -s dwi2mask/template_image.mif.gz "tmp template.mif.gz" ln -s dwi2mask/template_mask.mif.gz "tmp mask.mif.gz" @@ -11,9 +12,12 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask consensus "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -template "tmp template.mif.gz" "tmp mask.mif.gz" \ --masks "tmp masks.mif" +-masks "tmp masks.mif" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/fslbet_whitespace b/testing/scripts/tests/dwi2mask/fslbet_whitespace index 795c2e6e89..e5ee319dcf 100644 --- a/testing/scripts/tests/dwi2mask/fslbet_whitespace +++ b/testing/scripts/tests/dwi2mask/fslbet_whitespace @@ -3,12 +3,16 @@ # when making use of image paths that include whitespace characters # Make sure that output image appears at expected location rm -f "tmp out.mif" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask fslbet "tmp in.nii.gz" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/hdbet_whitespace b/testing/scripts/tests/dwi2mask/hdbet_whitespace index 2e20a91312..99aaa5d080 100644 --- a/testing/scripts/tests/dwi2mask/hdbet_whitespace +++ b/testing/scripts/tests/dwi2mask/hdbet_whitespace @@ -3,12 +3,16 @@ # when using image paths that include whitespace characters # Make sure that output image appears at expected location rm -f "tmp out.mif" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask hdbet "tmp in.nii.gz" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/dwi2mask/legacy_whitespace b/testing/scripts/tests/dwi2mask/legacy_whitespace index 2b1d314555..ec9ec77560 100644 --- a/testing/scripts/tests/dwi2mask/legacy_whitespace +++ b/testing/scripts/tests/dwi2mask/legacy_whitespace @@ -1,11 +1,16 @@ #!/bin/bash # Ensure correct operation of the "dwi2mask legacy" command # when image paths that include whitespace characters are used +rm -rf "tmp scratch" + ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask legacy "tmp in.nii.gz" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" testing_diff_image "tmp out.mif" dwi2mask/legacy.mif.gz diff --git a/testing/scripts/tests/dwi2mask/mean_whitespace b/testing/scripts/tests/dwi2mask/mean_whitespace index a284a0c1fb..967f35adee 100644 --- a/testing/scripts/tests/dwi2mask/mean_whitespace +++ b/testing/scripts/tests/dwi2mask/mean_whitespace @@ -1,11 +1,16 @@ #!/bin/bash # Verify "dwi2mask mean" algorithm # when image paths include whitespace characters +rm -rf "tmp scratch" + ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask mean "tmp in.nii.gz" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" testing_diff_image "tmp out.mif" dwi2mask/mean.mif.gz diff --git a/testing/scripts/tests/dwi2mask/mtnorm_whitespace b/testing/scripts/tests/dwi2mask/mtnorm_whitespace index 72762e9aff..f4d2c0be3e 100644 --- a/testing/scripts/tests/dwi2mask/mtnorm_whitespace +++ b/testing/scripts/tests/dwi2mask/mtnorm_whitespace @@ -1,14 +1,18 @@ #!/bin/bash # Ensure correct operation of the "dwi2mask mtnorm" command # when image paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask mtnorm "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ --tissuesum "tmp tissuesum.mif" +-tissuesum "tmp tissuesum.mif" \ +-scratch "tmp scratch" testing_diff_image "tmp out.mif" dwi2mask/mtnorm_default_mask.mif.gz testing_diff_image "tmp tissuesum.mif" dwi2mask/mtnorm_default_tissuesum.mif.gz -abs 1e-5 diff --git a/testing/scripts/tests/dwi2mask/synthstrip_whitespace b/testing/scripts/tests/dwi2mask/synthstrip_whitespace index b90a93addd..fe65db3ad0 100644 --- a/testing/scripts/tests/dwi2mask/synthstrip_whitespace +++ b/testing/scripts/tests/dwi2mask/synthstrip_whitespace @@ -3,14 +3,18 @@ # where image paths include whitespace characters # Check that expected output images appear rm -f "tmp out.mif" "tmp stripped.mif" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask synthstrip "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ --stripped "tmp stripped.mif" +-stripped "tmp stripped.mif" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif" ls | grep "^tmp stripped\.mif" diff --git a/testing/scripts/tests/dwi2mask/trace_whitespace b/testing/scripts/tests/dwi2mask/trace_whitespace index a6b4101c5b..c0dd480bbe 100644 --- a/testing/scripts/tests/dwi2mask/trace_whitespace +++ b/testing/scripts/tests/dwi2mask/trace_whitespace @@ -1,11 +1,16 @@ #!/bin/bash # Verify result of "dwi2mask trace" algorithm # when utilising images that have whitespaces in their paths +rm -rf "tmp scratch" + ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2mask trace "tmp in.nii.gz" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" testing_diff_image "tmp out.mif" dwi2mask/trace_default.mif.gz diff --git a/testing/scripts/tests/dwi2response/dhollander_whitespace b/testing/scripts/tests/dwi2response/dhollander_whitespace index a93023e362..e961a4a45a 100644 --- a/testing/scripts/tests/dwi2response/dhollander_whitespace +++ b/testing/scripts/tests/dwi2response/dhollander_whitespace @@ -1,14 +1,18 @@ #!/bin/bash # Verify successful execution of "dwi2response dhollander" # when filesystem paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2response dhollander "tmp in.nii.gz" "tmp wm.txt" "tmp gm.txt" "tmp csf.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ --voxels "tmp voxels.mif" +-voxels "tmp voxels.mif" \ +-scratch "tmp scratch" testing_diff_matrix "tmp wm.txt" dwi2response/dhollander/default_wm.txt -abs 1e-2 testing_diff_matrix "tmp gm.txt" dwi2response/dhollander/default_gm.txt -abs 1e-2 diff --git a/testing/scripts/tests/dwi2response/fa_whitespace b/testing/scripts/tests/dwi2response/fa_whitespace index c28d5a37c4..f2d1531a47 100644 --- a/testing/scripts/tests/dwi2response/fa_whitespace +++ b/testing/scripts/tests/dwi2response/fa_whitespace @@ -1,15 +1,19 @@ #!/bin/bash # Verify successful execution of "dwi2response fa" # when filesystem paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2response fa "tmp in.nii.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -voxels "tmp voxels.mif" \ --number 20 +-number 20 \ +-scratch "tmp scratch" testing_diff_matrix "tmp out.txt" dwi2response/fa/default.txt -abs 1e-2 testing_diff_image "tmp voxels.mif" dwi2response/fa/default.mif.gz diff --git a/testing/scripts/tests/dwi2response/manual_whitespace b/testing/scripts/tests/dwi2response/manual_whitespace index 440148d740..0beac6d145 100644 --- a/testing/scripts/tests/dwi2response/manual_whitespace +++ b/testing/scripts/tests/dwi2response/manual_whitespace @@ -1,6 +1,7 @@ #!/bin/bash # Verify successful execution of "dwi2response manual" # when filesystem paths include whitespace characters +rm -rf "tmp scratch" dwi2tensor BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ @@ -12,9 +13,12 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s dwi2response/fa/default.mif.gz "tmp voxels.mif.gz" +mkdir "tmp scratch" + dwi2response manual "tmp in.nii.gz" "tmp voxels.mif.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ --dirs "tmp dirs.mif" +-dirs "tmp dirs.mif" \ +-scratch "tmp scratch" testing_diff_matrix "tmp out.txt" dwi2response/manual/dirs.txt -abs 1e-2 diff --git a/testing/scripts/tests/dwi2response/msmt5tt_whitespace b/testing/scripts/tests/dwi2response/msmt5tt_whitespace index acc7db6ef7..cd5008bd2d 100644 --- a/testing/scripts/tests/dwi2response/msmt5tt_whitespace +++ b/testing/scripts/tests/dwi2response/msmt5tt_whitespace @@ -1,6 +1,7 @@ #!/bin/bash # Verify successful execution of "dwi2response msmt_5tt" # when filesystem paths include whitespace characters +rm -rf "tmp scratch" dwi2tensor BIDS/sub-01/dwi/sub-01_dwi.nii.gz - \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval \ @@ -12,12 +13,15 @@ ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/anat/sub-01_5TT.nii.gz "tmp 5TT.nii.gz" +mkdir "tmp scratch" + dwi2response msmt_5tt "tmp in.nii.gz" "tmp 5TT.nii.gz" \ "tmp wm.txt" "tmp gm.txt" "tmp csf.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -dirs "tmp dirs.mif" \ -voxels "tmp voxels.mif" \ --pvf 0.9 +-pvf 0.9 \ +-scratch "tmp scratch" testing_diff_matrix "tmp wm.txt" dwi2response/msmt_5tt/default_wm.txt -abs 1e-2 testing_diff_matrix "tmp gm.txt" dwi2response/msmt_5tt/default_gm.txt -abs 1e-2 diff --git a/testing/scripts/tests/dwi2response/tax_whitespace b/testing/scripts/tests/dwi2response/tax_whitespace index f152ce4398..748b9dfc00 100644 --- a/testing/scripts/tests/dwi2response/tax_whitespace +++ b/testing/scripts/tests/dwi2response/tax_whitespace @@ -1,14 +1,18 @@ #!/bin/bash # Verify successful execution of "dwi2response tax" # where image paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2response tax "tmp in.nii.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ --voxels "tmp voxels.mif" +-voxels "tmp voxels.mif" \ +-scratch "tmp scratch" testing_diff_matrix "tmp out.txt" dwi2response/tax/default.txt -abs 1e-2 testing_diff_image "tmp voxels.mif" dwi2response/tax/default.mif.gz diff --git a/testing/scripts/tests/dwi2response/tournier_whitespace b/testing/scripts/tests/dwi2response/tournier_whitespace index fe886ccefa..8591ac0418 100644 --- a/testing/scripts/tests/dwi2response/tournier_whitespace +++ b/testing/scripts/tests/dwi2response/tournier_whitespace @@ -1,16 +1,20 @@ #!/bin/bash # Verify successful execution of "dwi2response tournier" # where image paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwi2response tournier "tmp in.nii.gz" "tmp out.txt" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -voxels "tmp voxels.mif" \ -number 20 \ --iter_voxels 200 +-iter_voxels 200 \ +-scratch "tmp scratch" testing_diff_matrix "tmp out.txt" dwi2response/tournier/default.txt -abs 1e-2 testing_diff_image "tmp voxels.mif" dwi2response/tournier/default.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect/ants_whitespace b/testing/scripts/tests/dwibiascorrect/ants_whitespace index 2f33d38e87..97cfab5284 100644 --- a/testing/scripts/tests/dwibiascorrect/ants_whitespace +++ b/testing/scripts/tests/dwibiascorrect/ants_whitespace @@ -1,16 +1,20 @@ #!/bin/bash # Verify operation of "dwibiascorrect ants" algorithm # where image paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +mkdir "tmp scratch" + dwibiascorrect ants "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ --bias "tmp bias.mif" +-bias "tmp bias.mif" \ +-scratch "tmp scratch" testing_diff_header "tmp out.mif" dwibiascorrect/ants/default_out.mif.gz testing_diff_header "tmp bias.mif" dwibiascorrect/ants/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect/fsl_whitespace b/testing/scripts/tests/dwibiascorrect/fsl_whitespace index 105ac48a2e..61fcb96683 100644 --- a/testing/scripts/tests/dwibiascorrect/fsl_whitespace +++ b/testing/scripts/tests/dwibiascorrect/fsl_whitespace @@ -1,17 +1,20 @@ #!/bin/bash # Verify operation of "dwibiascorrect fsl" algorithm # where image paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" - ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +mkdir "tmp scratch" + dwibiascorrect fsl "tmp in.nii.gz" "tmp out.mif" \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ --bias "tmp bias.mif" +-bias "tmp bias.mif" \ +-scratch "tmp scratch" testing_diff_header "tmp out.mif" dwibiascorrect/fsl/default_out.mif.gz testing_diff_header "tmp bias.mif" dwibiascorrect/fsl/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace b/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace index fb3e0644a6..462aad72c9 100644 --- a/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace +++ b/testing/scripts/tests/dwibiascorrect/mtnorm_whitespace @@ -1,16 +1,20 @@ #!/bin/bash # Verify operation of "dwibiascorrect mtnorm" algorithm # where image paths contain whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +mkdir "tmp scratch" + dwibiascorrect mtnorm "tmp in.nii.gz" "tmp out.mif" \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ --bias "tmp bias.mif" +-bias "tmp bias.mif" \ +-scratch "tmp scratch" testing_diff_header "tmp out.mif" dwibiascorrect/mtnorm/default_out.mif.gz testing_diff_header "tmp bias.mif" dwibiascorrect/mtnorm/default_bias.mif.gz diff --git a/testing/scripts/tests/dwibiasnormmask/whitespace b/testing/scripts/tests/dwibiasnormmask/whitespace index aac1db8b5f..ab7c06dd5b 100644 --- a/testing/scripts/tests/dwibiasnormmask/whitespace +++ b/testing/scripts/tests/dwibiasnormmask/whitespace @@ -2,18 +2,22 @@ # Verify successful execution of command # when filesystem paths include whitespace characters rm -f "tmp *.mif" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +mkdir "tmp scratch" + dwibiasnormmask "tmp in.nii.gz" "tmp out.mif" "tmp mask.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -init_mask "tmp mask.nii.gz" \ -output_bias "tmp bias.mif" \ -output_scale "tmp scale.txt" \ --output_tissuesum "tmp tissuesum.mif" +-output_tissuesum "tmp tissuesum.mif" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" ls | grep "^tmp mask.mif$" diff --git a/testing/scripts/tests/dwicat/whitespace b/testing/scripts/tests/dwicat/whitespace index ffe1f28e22..babc3d3ff3 100644 --- a/testing/scripts/tests/dwicat/whitespace +++ b/testing/scripts/tests/dwicat/whitespace @@ -2,13 +2,17 @@ # Test operation of the dwicat command # where image paths include whitespace characters rm -f "tmp out.mif" +rm -rf "tmp scratch" mrconvert BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.mif" -force \ -fslgrad BIDS/sub-01/dwi/sub-01_dwi.bvec BIDS/sub-01/dwi/sub-01_dwi.bval ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +mkdir "tmp scratch" + dwicat "tmp in.mif" "tmp in.mif" "tmp out.mif" -force \ --mask "tmp mask.nii.gz" +-mask "tmp mask.nii.gz" \ +-scratch "tmp scratch" ls | grep "tmp out.mif" diff --git a/testing/scripts/tests/dwifslpreproc/whitespace b/testing/scripts/tests/dwifslpreproc/whitespace index eb3c00cec8..ddeb8e13fd 100644 --- a/testing/scripts/tests/dwifslpreproc/whitespace +++ b/testing/scripts/tests/dwifslpreproc/whitespace @@ -18,6 +18,8 @@ dwi2mask legacy BIDS/sub-04/dwi/sub-04_dwi.nii.gz tmpeddymask.mif -force \ -fslgrad BIDS/sub-04/dwi/sub-04_dwi.bvec BIDS/sub-04/dwi/sub-04_dwi.bval ln -s tmpeddymask.mif "tmp eddymask.mif" +mkdir "tmp scratch" + dwifslpreproc "tmp in.nii.gz" "tmp out.nii" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -json_import "tmp in.json" \ @@ -25,7 +27,8 @@ dwifslpreproc "tmp in.nii.gz" "tmp out.nii" -force \ -se_epi "tmp seepi.mif" \ -eddy_mask "tmp eddymask.mif" \ -eddyqc_text "tmp eddyqc" \ --export_grad_fsl "tmp out.bvec" "tmp out.bval" +-export_grad_fsl "tmp out.bvec" "tmp out.bval" \ +-scratch "tmp scratch" ls | grep "^tmp out\.nii$" ls | grep "^tmp out\.bvec$" diff --git a/testing/scripts/tests/dwigradcheck/whitespace b/testing/scripts/tests/dwigradcheck/whitespace index a0c60e0285..c68906a323 100644 --- a/testing/scripts/tests/dwigradcheck/whitespace +++ b/testing/scripts/tests/dwigradcheck/whitespace @@ -3,16 +3,20 @@ # when filesystem paths include whitespace characters # Ensure that output filesystem paths appear at the expected locations rm -f "tmp out.*" +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwigradcheck "tmp in.nii.gz" \ -fslgrad "tmp in.bvec" "tmp in.bval" \ -mask "tmp mask.nii.gz" \ --export_grad_fsl "tmp out.bvec" "tmp out.bval" +-export_grad_fsl "tmp out.bvec" "tmp out.bval" \ +-scratch "tmp scratch" ls | grep "^tmp out\.bvec$" ls | grep "^tmp out\.bval$" diff --git a/testing/scripts/tests/dwinormalise/group_whitespace b/testing/scripts/tests/dwinormalise/group_whitespace index f9362df963..e4918a2276 100644 --- a/testing/scripts/tests/dwinormalise/group_whitespace +++ b/testing/scripts/tests/dwinormalise/group_whitespace @@ -1,7 +1,7 @@ #!/bin/bash # Test the "dwinormalise group" algorithm # when filesystem paths include whitespace characters -rm -rf "tmp dwi" "tmp mask" +rm -rf "tmp dwi" "tmp mask" "tmp scratch" mkdir "tmp dwi" mkdir "tmp mask" @@ -13,7 +13,10 @@ mrconvert BIDS/sub-03/dwi/sub-03_dwi.nii.gz "tmp dwi/sub 03.mif" \ -fslgrad BIDS/sub-03/dwi/sub-03_dwi.bvec BIDS/sub-03/dwi/sub-03_dwi.bval mrconvert BIDS/sub-03/dwi/sub-03_brainmask.nii.gz "tmp mask/sub 03.mif" -dwinormalise group "tmp dwi/" "tmp mask/" "tmp group/" "tmp template.mif" "tmp mask.mif" -force +mkdir "tmp scratch" + +dwinormalise group "tmp dwi/" "tmp mask/" "tmp group/" "tmp template.mif" "tmp mask.mif" -force \ +-scratch "tmp scratch" testing_diff_image "tmp template.mif" dwinormalise/group/fa.mif.gz -abs 1e-3 testing_diff_image $(mrfilter "tmp mask.mif" smooth -) $(mrfilter dwinormalise/group/mask.mif.gz smooth -) -abs 0.3 diff --git a/testing/scripts/tests/dwinormalise/manual_whitespace b/testing/scripts/tests/dwinormalise/manual_whitespace index 2e6026dd3e..a10f84aead 100644 --- a/testing/scripts/tests/dwinormalise/manual_whitespace +++ b/testing/scripts/tests/dwinormalise/manual_whitespace @@ -1,14 +1,17 @@ #!/bin/bash # Verify default operation of the "dwinormalise manual" algorithm # where image paths include whitespace characters +rm -rf "tmp scratch" -# Input and output DWI series are both piped ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +mkdir "tmp scratch" + dwinormalise manual "tmp in.nii.gz" "tmp mask.nii.gz" "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" testing_diff_image "tmp out.mif" dwinormalise/manual/out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwinormalise/mtnorm_whitespace b/testing/scripts/tests/dwinormalise/mtnorm_whitespace index 63470f542f..fc7644823d 100644 --- a/testing/scripts/tests/dwinormalise/mtnorm_whitespace +++ b/testing/scripts/tests/dwinormalise/mtnorm_whitespace @@ -1,14 +1,18 @@ #!/bin/bash # Verify default operation of the "dwinormalise mtnorm" command # where filesystem paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp mask.nii.gz" +mkdir "tmp scratch" + dwinormalise mtnorm "tmp in.nii.gz" "tmp out.mif" -force \ -fslgrad "tmp in.bvec" "tmp in.bval" \ --mask "tmp mask.nii.gz" +-mask "tmp mask.nii.gz" \ +-scratch "tmp scratch" testing_diff_image "tmp out.mif" dwinormalise/mtnorm/masked_out.mif.gz -frac 1e-5 diff --git a/testing/scripts/tests/dwishellmath/whitespace b/testing/scripts/tests/dwishellmath/whitespace index 81012ff6a3..6d58a5edab 100644 --- a/testing/scripts/tests/dwishellmath/whitespace +++ b/testing/scripts/tests/dwishellmath/whitespace @@ -1,12 +1,16 @@ #!/bin/bash # Verify operation of the dwishellmath command # when image paths include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/dwi/sub-01_dwi.nii.gz "tmp in.nii.gz" ln -s BIDS/sub-01/dwi/sub-01_dwi.bvec "tmp in.bvec" ln -s BIDS/sub-01/dwi/sub-01_dwi.bval "tmp in.bval" +mkdir "tmp scratch" + dwishellmath "tmp in.nii.gz" mean "tmp out.mif" -force \ --fslgrad "tmp in.bvec" "tmp in.bval" +-fslgrad "tmp in.bvec" "tmp in.bval" \ +-scratch "tmp scratch" ls | grep "^tmp out\.mif$" diff --git a/testing/scripts/tests/labelsgmfirst/whitespace b/testing/scripts/tests/labelsgmfirst/whitespace index 35bf1391f3..a8831f3286 100644 --- a/testing/scripts/tests/labelsgmfirst/whitespace +++ b/testing/scripts/tests/labelsgmfirst/whitespace @@ -1,11 +1,15 @@ #!/bin/bash # Verify operation of command # when utilising image paths that include whitespace characters +rm -rf "tmp scratch" ln -s BIDS/sub-01/anat/sub-01_parc-desikan_indices.nii.gz "tmp indices.nii.gz" ln -s BIDS/sub-01/anat/sub-01_T1w.nii.gz "tmp T1w.nii.gz" ln -s BIDS/parc-desikan_lookup.txt "tmp lookup.txt" -labelsgmfirst "tmp indices.nii.gz" "tmp T1w.nii.gz" "tmp lookup.txt" "tmp out.mif" -force +mkdir "tmp scratch" + +labelsgmfirst "tmp indices.nii.gz" "tmp T1w.nii.gz" "tmp lookup.txt" "tmp out.mif" -force \ +-scratch "tmp scratch" testing_diff_header "tmp out.mif" labelsgmfirst/default.mif.gz diff --git a/testing/scripts/tests/mask2glass/whitespace b/testing/scripts/tests/mask2glass/whitespace index aab638e4e1..18a8495d6c 100644 --- a/testing/scripts/tests/mask2glass/whitespace +++ b/testing/scripts/tests/mask2glass/whitespace @@ -1,8 +1,12 @@ #!/bin/bash # Verify operation of the "mask2glass" command # where image paths include whitespace characters +rm -rf "tmp scratch" + ln -s BIDS/sub-01/dwi/sub-01_brainmask.nii.gz "tmp in.nii.gz" +mkdir "tmp scratch" -mask2glass "tmp in.nii.gz" "tmp out.mif" -force +mask2glass "tmp in.nii.gz" "tmp out.mif" -force \ +-scratch "tmp scratch" testing_diff_image "tmp out.mif" mask2glass/out.mif.gz diff --git a/testing/scripts/tests/population_template/whitespace b/testing/scripts/tests/population_template/whitespace index e73b741328..0cf7620e17 100644 --- a/testing/scripts/tests/population_template/whitespace +++ b/testing/scripts/tests/population_template/whitespace @@ -3,7 +3,7 @@ # where filesystem paths include whitespace characters rm -rf "tmp *" -mkdir "tmp fa" "tmp mask" +mkdir "tmp fa" "tmp mask" "tmp scratch" dwi2tensor BIDS/sub-02/dwi/sub-02_dwi.nii.gz - \ -fslgrad BIDS/sub-02/dwi/sub-02_dwi.bvec BIDS/sub-02/dwi/sub-02_dwi.bval \ @@ -22,7 +22,8 @@ population_template "tmp fa/" "tmp template.mif" -force \ -mask_dir "tmp mask/" \ -template_mask "tmp mask.mif" \ -warp_dir "tmp warps/" \ --linear_transformations_dir "tmp linear_transformations/" +-linear_transformations_dir "tmp linear_transformations/" \ +-scratch "tmp scratch" testing_diff_image "tmp template.mif" population_template/fa_masked_template.mif.gz -abs 0.01 testing_diff_image $(mrfilter "tmp mask.mif" smooth -) $(mrfilter population_template/fa_masked_mask.mif.gz smooth -) -abs 0.3 diff --git a/testing/scripts/tests/responsemean/whitespace b/testing/scripts/tests/responsemean/whitespace index 92a5cacf29..41fa9d4609 100644 --- a/testing/scripts/tests/responsemean/whitespace +++ b/testing/scripts/tests/responsemean/whitespace @@ -3,5 +3,5 @@ ln -s BIDS/sub-02/dwi/sub-02_tissue-WM_response.txt "tmp response2.txt" ln -s BIDS/sub-03/dwi/sub-03_tissue-WM_response.txt "tmp response3.txt" -responsemean "tmp response2txt" "tmp response3.txt" "tmp out.txt" -force +responsemean "tmp response2.txt" "tmp response3.txt" "tmp out.txt" -force testing_diff_matrix "tmp out.txt" responsemean/out.txt -abs 0.001 From ef5b8c598291336d763b810a28113025813c74dd Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sat, 18 May 2024 17:30:54 +1000 Subject: [PATCH 71/75] mrtrix3.app: Fix argparse exception on existing output filesystem path --- python/lib/mrtrix3/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 25dde69d45..5f82b56656 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -604,7 +604,8 @@ def check_output(self, item_type='path'): warn(f'Output {item_type} "{str(self)}" already exists; ' 'will be overwritten at script completion') else: - raise argparse.ArgumentError(f'Output {item_type} "{str(self)}" already exists ' + raise argparse.ArgumentError(CMDLINE, + f'Output {item_type} "{str(self)}" already exists ' '(use -force option to force overwrite)') class _UserFileOutPathExtras(_UserOutPathExtras): def __init__(self, *args, **kwargs): @@ -630,7 +631,8 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ return except FileExistsError: if not FORCE_OVERWRITE: - raise argparse.ArgumentError(f'Output directory "{str(self)}" already exists ' # pylint: disable=raise-missing-from + raise argparse.ArgumentError(CMDLINE, # pylint: disable=raise-missing-from + f'Output directory "{str(self)}" already exists ' '(use -force option to force overwrite)') # Various callable types for use as argparse argument types From 40fdc8e2a88710a21e98885dba9b2b8665d0f473 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sat, 18 May 2024 17:34:41 +1000 Subject: [PATCH 72/75] Testing: Fix execution of bash unit tests In #2678 the working directory for unit tests is set to new directory testing/data/. But in #2865, bash tests are executed as-is, rather than being imported and executed on a per-line basis. Resolving these requires setting the absolute path to the test bash script. --- testing/unit_tests/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/unit_tests/CMakeLists.txt b/testing/unit_tests/CMakeLists.txt index 72b5c219a6..06d56c34d4 100644 --- a/testing/unit_tests/CMakeLists.txt +++ b/testing/unit_tests/CMakeLists.txt @@ -64,10 +64,11 @@ function(add_cpp_unit_test FILE_SRC) set_tests_properties(unittest_${NAME} PROPERTIES LABELS "unittest") endfunction() +include(BashTests) function (add_bash_unit_test FILE_SRC) get_filename_component(NAME ${FILE_SRC} NAME_WE) add_bash_test( - FILE_PATH "${FILE_SRC}" + FILE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${FILE_SRC}" PREFIX "unittest" WORKING_DIRECTORY ${DATA_DIR} EXEC_DIRECTORIES "${EXEC_DIRS}" From b3d98c10a9cf55a0eb5e1f81881798cfcbf10c56 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sat, 18 May 2024 17:40:02 +1000 Subject: [PATCH 73/75] Testing: Add comments to CLI tests --- testing/unit_tests/cpp_cli | 13 +++++++++++++ testing/unit_tests/python_cli | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/testing/unit_tests/cpp_cli b/testing/unit_tests/cpp_cli index 2aa9fdcc52..48ae13375d 100644 --- a/testing/unit_tests/cpp_cli +++ b/testing/unit_tests/cpp_cli @@ -1,8 +1,16 @@ +#!/bin/bash +# Multiple tests for the C++ binary command-line interface + +# Test using all options at once mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_cpp_cli -flag -text my_text -choice One -bool false -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck + +# Test export of interface to various file formats testing_cpp_cli -help | tail -n +3 > tmp.txt && diff -a --strip-trailing-cr tmp.txt cpp_cli/help.txt && rm -f tmp.txt testing_cpp_cli __print_full_usage__ > tmp.txt && diff -a --strip-trailing-cr tmp.txt cpp_cli/full_usage.txt && rm -f tmp.txt testing_cpp_cli __print_usage_markdown__ > tmp.md && diff -a --strip-trailing-cr tmp.md cpp_cli/markdown.md && rm -f tmp.md testing_cpp_cli __print_usage_rst__ > tmp.rst && diff -a --strip-trailing-cr tmp.rst cpp_cli/restructured_text.rst && rm -f tmp.rst + +# Test suitable handling of valid and invalid inputs to various argument types testing_cpp_cli -bool false testing_cpp_cli -bool False testing_cpp_cli -bool FALSE @@ -26,6 +34,10 @@ testing_cpp_cli -float_bound 1.1 && false || true testing_cpp_cli -int_seq 0.1,0.2,0.3 && false || true testing_cpp_cli -int_seq Not,An,Int,Seq && false || true testing_cpp_cli -float_seq Not,A,Float,Seq && false || true + +# Test interfaces relating to filesystem paths: +# - Make sure that command fails if expected input is not present +# - Make sure that existing outputs either succeed or fail depending on the presence of the -force option rm -rf tmp-dirin/ && testing_cpp_cli -dir_in tmp-dirin/ && false || true trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_cpp_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force option to force overwrite" trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_cpp_cli -dir_out tmp-dirout/ -force @@ -36,3 +48,4 @@ rm -f tmp-tracksin.tck && testing_cpp_cli -tracks_in tmp-tracksin.tck && false | trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && testing_cpp_cli -tracks_in tmp-filein.txt && false || true trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_cpp_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force option to force overwrite" trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_cpp_cli -tracks_out tmp-tracksout.tck -force + diff --git a/testing/unit_tests/python_cli b/testing/unit_tests/python_cli index 250b5072dd..65edf71a7e 100644 --- a/testing/unit_tests/python_cli +++ b/testing/unit_tests/python_cli @@ -1,8 +1,16 @@ +#!/bin/bash +# Various tests of the functionality of the Pyhon command-line interface + +# Utilisation of all argument types mkdir -p tmp-dirin/ && touch tmp-filein.txt && touch tmp-tracksin.tck && testing_python_cli -flag -string_implicit my_implicit_string -string_explicit my_explicit_string -choice One -bool false -int_builtin 0 -float_builtin 0.0 -int_unbound 0 -int_nonneg 1 -int_bound 50 -float_unbound 0.0 -float_nonneg 1.0 -float_bound 0.5 -int_seq 1,2,3 -float_seq 0.1,0.2,0.3 -dir_in tmp-dirin/ -dir_out tmp-dirout/ -file_in tmp-filein.txt -file_out tmp-fileout.txt -tracks_in tmp-tracksin.tck -tracks_out tmp-tracksout.tck -various my_various && rm -rf tmp-dirin/ && rm -f tmp-filein.txt && rm -f tmp-tracksin.tck + +# Ensure that export of the command-line interfaces in various file formats obeys expectations testing_python_cli -help | tail -n +3 > tmp.txt && diff -a --strip-trailing-cr tmp.txt python_cli/help.txt && rm -f tmp.txt testing_python_cli __print_full_usage__ > tmp.txt && diff -a --strip-trailing-cr tmp.txt python_cli/full_usage.txt && rm -f tmp.txt testing_python_cli __print_usage_markdown__ > tmp.md && diff -a --strip-trailing-cr tmp.md python_cli/markdown.md && rm -f tmp.md testing_python_cli __print_usage_rst__ > tmp.rst && diff -a --strip-trailing-cr tmp.rst python_cli/restructured_text.rst && rm -f tmp.rst + +# Test various argument types for both appropriate and inappropriate inputs testing_python_cli -bool false testing_python_cli -bool False testing_python_cli -bool FALSE @@ -26,6 +34,11 @@ testing_python_cli -float_bound 1.1 && false || true testing_python_cli -int_seq 0.1,0.2,0.3 && false || true testing_python_cli -int_seq Not,An,Int,Seq && false || true testing_python_cli -float_seq Not,A,Float,Seq && false || true + +# Tests relating to filesystem paths: +# - Ensure that absent inputs result in appropriate error +# - Ensure that pre-existing output paths are handled accordingly +# based on presence or absence of -force option rm -rf tmp-dirin/ && testing_python_cli -dir_in tmp-dirin/ && false || true trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force option to force overwrite" trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ -force @@ -36,3 +49,4 @@ rm -f tmp-tracksin.tck && testing_python_cli -tracks_in tmp-tracksin.tck && fals trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && testing_python_cli -tracks_in tmp-filein.txt && false || true trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force option to force overwrite" trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck -force + From 4d8f7668edbe35de7572f4c238205e5c7a6cadd7 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sat, 18 May 2024 22:33:37 +1000 Subject: [PATCH 74/75] Python CLI & testing fixes - Restore solution in #2845 not properly propagated through prior merge conflict. - Use FileExistsError when checking for pre-existing output files / directories, and catch it to yield a well-formatted error message. - Update CLI test data to reflect changes in 05b68d5. - Modify tests that check for command error due to inappropriate CLI usage. --- cmake/BashTests.cmake | 3 +- python/lib/mrtrix3/app.py | 27 +++++++----- testing/data/python_cli/full_usage.txt | 4 +- testing/data/python_cli/help.txt | 3 +- testing/data/python_cli/markdown.md | 2 +- testing/data/python_cli/restructured_text.rst | 2 +- testing/unit_tests/cpp_cli | 39 ++++++++--------- testing/unit_tests/python_cli | 42 +++++++++---------- 8 files changed, 62 insertions(+), 60 deletions(-) diff --git a/cmake/BashTests.cmake b/cmake/BashTests.cmake index 81604896a2..573c801a01 100644 --- a/cmake/BashTests.cmake +++ b/cmake/BashTests.cmake @@ -44,12 +44,11 @@ function(add_bash_test) -D FILE_PATH=${file_path} -D CLEANUP_CMD=${cleanup_cmd} -D WORKING_DIRECTORY=${working_directory} - -D ENVIRONMENT=${environment} -P ${PROJECT_SOURCE_DIR}/cmake/RunTest.cmake ) set_tests_properties(${test_name} PROPERTIES - ENVIRONMENT "PATH=${exec_directories}" + ENVIRONMENT "PATH=${exec_directories};${environment}" ) if(labels) set_tests_properties(${test_name} PROPERTIES LABELS "${labels}") diff --git a/python/lib/mrtrix3/app.py b/python/lib/mrtrix3/app.py index 5f82b56656..27b6189cbe 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/lib/mrtrix3/app.py @@ -185,10 +185,16 @@ def _execute(module): #pylint: disable=unused-variable # Now that FORCE_OVERWRITE has been set, # check any user-specified output paths - for key in vars(ARGS): - value = getattr(ARGS, key) - if isinstance(value, Parser._UserOutPathExtras): # pylint: disable=protected-access - value.check_output() + try: + for key in vars(ARGS): + value = getattr(ARGS, key) + if isinstance(value, Parser._UserOutPathExtras): # pylint: disable=protected-access + value.check_output() + except FileExistsError as exception: + sys.stderr.write('\n') + sys.stderr.write(f'{EXEC_NAME}: {ANSI.error}[ERROR] {exception}{ANSI.clear}\n') + sys.stderr.flush() + sys.exit(1) # ANSI settings may have been altered at the command-line setup_ansi() @@ -604,9 +610,8 @@ def check_output(self, item_type='path'): warn(f'Output {item_type} "{str(self)}" already exists; ' 'will be overwritten at script completion') else: - raise argparse.ArgumentError(CMDLINE, - f'Output {item_type} "{str(self)}" already exists ' - '(use -force option to force overwrite)') + raise FileExistsError(f'Output {item_type} "{str(self)}" already exists ' + '(use -force option to force overwrite)') class _UserFileOutPathExtras(_UserOutPathExtras): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) @@ -631,9 +636,9 @@ def mkdir(self, mode=0o777): # pylint: disable=arguments-differ return except FileExistsError: if not FORCE_OVERWRITE: - raise argparse.ArgumentError(CMDLINE, # pylint: disable=raise-missing-from - f'Output directory "{str(self)}" already exists ' - '(use -force option to force overwrite)') + # pylint: disable=raise-missing-from + raise FileExistsError(f'Output directory "{str(self)}" already exists ' + '(use -force option to force overwrite)') # Various callable types for use as argparse argument types class CustomTypeBase: @@ -654,7 +659,7 @@ def __call__(self, input_value): try: processed_value = int(processed_value) except ValueError as exc: - raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as boolean value"') from exc + raise argparse.ArgumentTypeError(f'Could not interpret "{input_value}" as boolean value') from exc return bool(processed_value) @staticmethod def _legacytypestring(): diff --git a/testing/data/python_cli/full_usage.txt b/testing/data/python_cli/full_usage.txt index da473fffc0..1d3a4a3d8f 100644 --- a/testing/data/python_cli/full_usage.txt +++ b/testing/data/python_cli/full_usage.txt @@ -100,8 +100,8 @@ ARGUMENT float_builtin 0 0 FLOAT -inf inf OPTION -nocleanup 1 0 do not delete intermediate files during script execution, and do not delete scratch directory at script completion. OPTION -scratch 1 0 -manually specify the path in which to generate the scratch directory. -ARGUMENT /path/to/scratch/ 0 0 DIROUT +manually specify an existing directory in which to generate the scratch directory. +ARGUMENT /path/to/scratch/ 0 0 DIRIN OPTION -continue 1 0 continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. ARGUMENT ScratchDir 0 0 VARIOUS diff --git a/testing/data/python_cli/help.txt b/testing/data/python_cli/help.txt index d8075efa2f..4a1431e2df 100644 --- a/testing/data/python_cli/help.txt +++ b/testing/data/python_cli/help.txt @@ -120,7 +120,8 @@ AAddddiittiioonnaall  ssttaannddaarrdd  ooppttiioonns scratch directory at script completion. _-_s_c_r_a_t_c_h /path/to/scratch/ - manually specify the path in which to generate the scratch directory. + manually specify an existing directory in which to generate the scratch + directory. _-_c_o_n_t_i_n_u_e ScratchDir LastFile continue the script from a previous execution; must provide the scratch diff --git a/testing/data/python_cli/markdown.md b/testing/data/python_cli/markdown.md index 62e78885d6..7b6fe2a13d 100644 --- a/testing/data/python_cli/markdown.md +++ b/testing/data/python_cli/markdown.md @@ -82,7 +82,7 @@ Test operation of the Python command-line interface + **--nocleanup**
do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -+ **--scratch /path/to/scratch/**
manually specify the path in which to generate the scratch directory. ++ **--scratch /path/to/scratch/**
manually specify an existing directory in which to generate the scratch directory. + **--continue ScratchDir LastFile**
continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/testing/data/python_cli/restructured_text.rst b/testing/data/python_cli/restructured_text.rst index 094244ec92..b5f6cb34e4 100644 --- a/testing/data/python_cli/restructured_text.rst +++ b/testing/data/python_cli/restructured_text.rst @@ -97,7 +97,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/testing/unit_tests/cpp_cli b/testing/unit_tests/cpp_cli index 48ae13375d..3bdd0b1d47 100644 --- a/testing/unit_tests/cpp_cli +++ b/testing/unit_tests/cpp_cli @@ -20,32 +20,29 @@ testing_cpp_cli -bool TRUE testing_cpp_cli -bool 0 testing_cpp_cli -bool 1 testing_cpp_cli -bool 2 -testing_cpp_cli -bool NotABool && false || true -testing_cpp_cli -int_builtin 0.1 && false || true -testing_cpp_cli -int_builtin NotAnInt && false || true -testing_cpp_cli -int_unbound 0.1 && false || true -testing_cpp_cli -int_unbound NotAnInt && false || true -testing_cpp_cli -int_nonneg -1 && false || true -testing_cpp_cli -int_bound 101 && false || true -testing_cpp_cli -float_builtin NotAFloat && false || true -testing_cpp_cli -float_unbound NotAFloat && false || true -testing_cpp_cli -float_nonneg -0.1 && false || true -testing_cpp_cli -float_bound 1.1 && false || true -testing_cpp_cli -int_seq 0.1,0.2,0.3 && false || true -testing_cpp_cli -int_seq Not,An,Int,Seq && false || true -testing_cpp_cli -float_seq Not,A,Float,Seq && false || true +! testing_cpp_cli -bool NotABool +! testing_cpp_cli -int_unbound 0.1 +! testing_cpp_cli -int_unbound NotAnInt +! testing_cpp_cli -int_nonneg -1 +! testing_cpp_cli -int_bound 101 +! testing_cpp_cli -float_unbound NotAFloat +! testing_cpp_cli -float_nonneg -0.1 +! testing_cpp_cli -float_bound 1.1 +! testing_cpp_cli -int_seq 0.1,0.2,0.3 +! testing_cpp_cli -int_seq Not,An,Int,Seq +! testing_cpp_cli -float_seq Not,A,Float,Seq # Test interfaces relating to filesystem paths: # - Make sure that command fails if expected input is not present # - Make sure that existing outputs either succeed or fail depending on the presence of the -force option -rm -rf tmp-dirin/ && testing_cpp_cli -dir_in tmp-dirin/ && false || true -trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_cpp_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force option to force overwrite" +rm -rf tmp-dirin/ && ! testing_cpp_cli -dir_in tmp-dirin/ +trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && ! testing_cpp_cli -dir_out tmp-dirout/ trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_cpp_cli -dir_out tmp-dirout/ -force -rm -f tmp-filein.txt && testing_cpp_cli -file_in tmp-filein.txt && false || true -trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_cpp_cli -file_out tmp-fileout.txt 2>&1 | grep -q "use -force option to force overwrite" +rm -f tmp-filein.txt && ! testing_cpp_cli -file_in tmp-filein.txt +trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && ! testing_cpp_cli -file_out tmp-fileout.txt trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_cpp_cli -file_out tmp-fileout.txt -force -rm -f tmp-tracksin.tck && testing_cpp_cli -tracks_in tmp-tracksin.tck && false || true -trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && testing_cpp_cli -tracks_in tmp-filein.txt && false || true -trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_cpp_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force option to force overwrite" +rm -f tmp-tracksin.tck && ! testing_cpp_cli -tracks_in tmp-tracksin.tck +trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && ! testing_cpp_cli -tracks_in tmp-filein.txt +trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && ! testing_cpp_cli -tracks_out tmp-tracksout.tck trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_cpp_cli -tracks_out tmp-tracksout.tck -force diff --git a/testing/unit_tests/python_cli b/testing/unit_tests/python_cli index 65edf71a7e..d45cbd6ad3 100644 --- a/testing/unit_tests/python_cli +++ b/testing/unit_tests/python_cli @@ -20,33 +20,33 @@ testing_python_cli -bool TRUE testing_python_cli -bool 0 testing_python_cli -bool 1 testing_python_cli -bool 2 -testing_python_cli -bool NotABool && false || true -testing_python_cli -int_builtin 0.1 && false || true -testing_python_cli -int_builtin NotAnInt && false || true -testing_python_cli -int_unbound 0.1 && false || true -testing_python_cli -int_unbound NotAnInt && false || true -testing_python_cli -int_nonneg -1 && false || true -testing_python_cli -int_bound 101 && false || true -testing_python_cli -float_builtin NotAFloat && false || true -testing_python_cli -float_unbound NotAFloat && false || true -testing_python_cli -float_nonneg -0.1 && false || true -testing_python_cli -float_bound 1.1 && false || true -testing_python_cli -int_seq 0.1,0.2,0.3 && false || true -testing_python_cli -int_seq Not,An,Int,Seq && false || true -testing_python_cli -float_seq Not,A,Float,Seq && false || true +! testing_python_cli -bool NotABool +! testing_python_cli -int_builtin 0.1 +! testing_python_cli -int_builtin NotAnInt +! testing_python_cli -int_unbound 0.1 +! testing_python_cli -int_unbound NotAnInt +! testing_python_cli -int_nonneg -1 +! testing_python_cli -int_bound 101 +! testing_python_cli -float_builtin NotAFloat +! testing_python_cli -float_unbound NotAFloat +! testing_python_cli -float_nonneg -0.1 +! testing_python_cli -float_bound 1.1 +! testing_python_cli -int_seq 0.1,0.2,0.3 +! testing_python_cli -int_seq Not,An,Int,Seq +! testing_python_cli -float_seq Not,A,Float,Seq # Tests relating to filesystem paths: # - Ensure that absent inputs result in appropriate error # - Ensure that pre-existing output paths are handled accordingly # based on presence or absence of -force option -rm -rf tmp-dirin/ && testing_python_cli -dir_in tmp-dirin/ && false || true -trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ 2>&1 | grep -q "use -force option to force overwrite" +rm -rf tmp-dirin/ && ! testing_python_cli -dir_in tmp-dirin/ +trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && ! testing_python_cli -dir_out tmp-dirout/ trap "rm -rf tmp-dirout/" EXIT; mkdir -p tmp-dirout/ && testing_python_cli -dir_out tmp-dirout/ -force -rm -f tmp-filein.txt && testing_python_cli -file_in tmp-filein.txt && false || true -trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt 2>&1 | grep -q "use -force option to force overwrite" +rm -f tmp-filein.txt && ! testing_python_cli -file_in tmp-filein.txt +trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && ! testing_python_cli -file_out tmp-fileout.txt trap "rm -f tmp-fileout.txt" EXIT; touch tmp-fileout.txt && testing_python_cli -file_out tmp-fileout.txt -force -rm -f tmp-tracksin.tck && testing_python_cli -tracks_in tmp-tracksin.tck && false || true -trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && testing_python_cli -tracks_in tmp-filein.txt && false || true -trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck 2>&1 | grep -q "use -force option to force overwrite" +rm -f tmp-tracksin.tck && ! testing_python_cli -tracks_in tmp-tracksin.tck +trap "rm -f tmp-filein.txt" EXIT; touch tmp-filein.txt && ! testing_python_cli -tracks_in tmp-filein.txt +trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && ! testing_python_cli -tracks_out tmp-tracksout.tck trap "rm -f tmp-tracksout.txt" EXIT; touch tmp-tracksout.tck && testing_python_cli -tracks_out tmp-tracksout.tck -force From de5ebe70c4cfc2ecb5923650fe1c69128746b939 Mon Sep 17 00:00:00 2001 From: MRtrixBot Date: Tue, 21 May 2024 09:52:24 +1000 Subject: [PATCH 75/75] Docs: Auto update for changes to -scratch option --- docs/reference/commands/5ttgen.rst | 10 ++++---- docs/reference/commands/dwi2mask.rst | 24 +++++++++---------- docs/reference/commands/dwi2response.rst | 14 +++++------ docs/reference/commands/dwibiascorrect.rst | 8 +++---- docs/reference/commands/dwibiasnormmask.rst | 2 +- docs/reference/commands/dwicat.rst | 2 +- docs/reference/commands/dwifslpreproc.rst | 2 +- docs/reference/commands/dwigradcheck.rst | 2 +- docs/reference/commands/dwinormalise.rst | 8 +++---- docs/reference/commands/dwishellmath.rst | 2 +- docs/reference/commands/for_each.rst | 2 +- docs/reference/commands/labelsgmfirst.rst | 2 +- docs/reference/commands/mask2glass.rst | 2 +- docs/reference/commands/mrtrix_cleanup.rst | 2 +- .../commands/population_template.rst | 2 +- docs/reference/commands/responsemean.rst | 2 +- 16 files changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index 435669fd4a..995f47a624 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -39,7 +39,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -130,7 +130,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -225,7 +225,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -319,7 +319,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -413,7 +413,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index d4ca637b07..26d50f6a85 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -40,7 +40,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -149,7 +149,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -240,7 +240,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -352,7 +352,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -451,7 +451,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -548,7 +548,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -639,7 +639,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -727,7 +727,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -818,7 +818,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -922,7 +922,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -1028,7 +1028,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -1128,7 +1128,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index 472cf7352e..23764dd20d 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -56,7 +56,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -173,7 +173,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -281,7 +281,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -384,7 +384,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -495,7 +495,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -601,7 +601,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -709,7 +709,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwibiascorrect.rst b/docs/reference/commands/dwibiascorrect.rst index 0cba332966..e8f8fdf49a 100644 --- a/docs/reference/commands/dwibiascorrect.rst +++ b/docs/reference/commands/dwibiascorrect.rst @@ -45,7 +45,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -145,7 +145,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -243,7 +243,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -352,7 +352,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwibiasnormmask.rst b/docs/reference/commands/dwibiasnormmask.rst index 0675473834..2a8de8f063 100644 --- a/docs/reference/commands/dwibiasnormmask.rst +++ b/docs/reference/commands/dwibiasnormmask.rst @@ -73,7 +73,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwicat.rst b/docs/reference/commands/dwicat.rst index 7cfbd3c15d..950660bcb7 100644 --- a/docs/reference/commands/dwicat.rst +++ b/docs/reference/commands/dwicat.rst @@ -39,7 +39,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwifslpreproc.rst b/docs/reference/commands/dwifslpreproc.rst index ac43dd280e..cc22c14a7b 100644 --- a/docs/reference/commands/dwifslpreproc.rst +++ b/docs/reference/commands/dwifslpreproc.rst @@ -133,7 +133,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwigradcheck.rst b/docs/reference/commands/dwigradcheck.rst index 1317b9e73c..1f0898bd47 100644 --- a/docs/reference/commands/dwigradcheck.rst +++ b/docs/reference/commands/dwigradcheck.rst @@ -51,7 +51,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwinormalise.rst b/docs/reference/commands/dwinormalise.rst index 4797b73105..f7fd5098fe 100644 --- a/docs/reference/commands/dwinormalise.rst +++ b/docs/reference/commands/dwinormalise.rst @@ -30,7 +30,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -119,7 +119,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -208,7 +208,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. @@ -312,7 +312,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/dwishellmath.rst b/docs/reference/commands/dwishellmath.rst index 4c6611b63f..c4e30e73bf 100644 --- a/docs/reference/commands/dwishellmath.rst +++ b/docs/reference/commands/dwishellmath.rst @@ -46,7 +46,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/for_each.rst b/docs/reference/commands/for_each.rst index b09948c876..d3c5e7d148 100644 --- a/docs/reference/commands/for_each.rst +++ b/docs/reference/commands/for_each.rst @@ -80,7 +80,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/labelsgmfirst.rst b/docs/reference/commands/labelsgmfirst.rst index f99a856362..32438e3367 100644 --- a/docs/reference/commands/labelsgmfirst.rst +++ b/docs/reference/commands/labelsgmfirst.rst @@ -32,7 +32,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/mask2glass.rst b/docs/reference/commands/mask2glass.rst index 8257bf5f85..cf6acc068c 100644 --- a/docs/reference/commands/mask2glass.rst +++ b/docs/reference/commands/mask2glass.rst @@ -39,7 +39,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/mrtrix_cleanup.rst b/docs/reference/commands/mrtrix_cleanup.rst index f73b1465c4..f592b9c39f 100644 --- a/docs/reference/commands/mrtrix_cleanup.rst +++ b/docs/reference/commands/mrtrix_cleanup.rst @@ -38,7 +38,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/population_template.rst b/docs/reference/commands/population_template.rst index ba9fbae166..daddfce743 100644 --- a/docs/reference/commands/population_template.rst +++ b/docs/reference/commands/population_template.rst @@ -120,7 +120,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file. diff --git a/docs/reference/commands/responsemean.rst b/docs/reference/commands/responsemean.rst index 3d5663cbfb..ff89863419 100644 --- a/docs/reference/commands/responsemean.rst +++ b/docs/reference/commands/responsemean.rst @@ -50,7 +50,7 @@ Additional standard options for Python scripts - **-nocleanup** do not delete intermediate files during script execution, and do not delete scratch directory at script completion. -- **-scratch /path/to/scratch/** manually specify the path in which to generate the scratch directory. +- **-scratch /path/to/scratch/** manually specify an existing directory in which to generate the scratch directory. - **-continue ScratchDir LastFile** continue the script from a previous execution; must provide the scratch directory path, and the name of the last successfully-generated file.