From cd8bbfb17ad0342f13c6384cb6aba141d4317785 Mon Sep 17 00:00:00 2001 From: Ankita Sanil Date: Mon, 6 Feb 2023 11:25:50 +1100 Subject: [PATCH 001/182] 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 002/182] 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 003/182] 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 004/182] 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 005/182] 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 006/182] 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 007/182] 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 008/182] 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 009/182] 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 010/182] 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 011/182] 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 012/182] 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 013/182] 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 014/182] 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 015/182] 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 016/182] 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 017/182] 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 018/182] 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 019/182] 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 020/182] 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 021/182] 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 022/182] 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 023/182] 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 024/182] 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 025/182] 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 026/182] 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 027/182] 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 028/182] 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 4208d2e96c2817685913f578e3b200fe074ad60f Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Wed, 25 Oct 2023 12:14:39 +0200 Subject: [PATCH 029/182] Initial support for macOS app bundles for GUI apps This can absorb some of the logic around this currently found in https://github.com/MRtrix3/macos-installer/ and/or https://github.com/MRtrix3/mrtrix3/tree/app_bundles --- cmd/CMakeLists.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index 10d0593600..36a6ba44e4 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -14,11 +14,13 @@ function(add_cmd CMD_SRC IS_GUI) $,mrtrix::gui,mrtrix::headless> mrtrix::exec-version-lib ) - set_target_properties(${CMD_NAME} PROPERTIES + set_target_properties(${CMD_NAME} PROPERTIES + MACOSX_BUNDLE ${IS_GUI} + MACOSX_BUNDLE_ICON_FILE mrview.icns LINK_DEPENDS_NO_SHARED true RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin ) - install(TARGETS ${CMD_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(TARGETS ${CMD_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}) endfunction() @@ -30,4 +32,4 @@ if(MRTRIX_BUILD_GUI) foreach(CMD ${GUI_CMD_SRCS}) add_cmd(${CMD} TRUE) endforeach(CMD) -endif() \ No newline at end of file +endif() From ee13d9d05a11f13d622c8c0fc33ea2581c915e92 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Thu, 26 Oct 2023 11:08:20 +0200 Subject: [PATCH 030/182] Clean up app bundle generation --- CMakeLists.txt | 3 + cmake/bundle/mrview.plist.in | 156 +++++++++++++++++++++++++++++++++++ cmake/bundle/shview.plist.in | 20 +++++ cmd/CMakeLists.txt | 7 +- 4 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 cmake/bundle/mrview.plist.in create mode 100644 cmake/bundle/shview.plist.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 841f322a13..1207d802ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(mrtrix3 VERSION 3.0.4) include(GNUInstallDirs) +set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15.0") +string(TIMESTAMP CURRENT_YEAR "%Y") + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") set(MRTRIX_BASE_VERSION "${CMAKE_PROJECT_VERSION}") diff --git a/cmake/bundle/mrview.plist.in b/cmake/bundle/mrview.plist.in new file mode 100644 index 0000000000..af08829e1d --- /dev/null +++ b/cmake/bundle/mrview.plist.in @@ -0,0 +1,156 @@ + + + + + CFBundleInfoDictionaryVersion 6.0 + CFBundleDevelopmentRegion en + CFBundleName MRView + CFBundleDisplayName MRView + CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIconFile ${MACOSX_BUNDLE_EXECUTABLE_NAME}.icns + CFBundleIdentifier org.mrtrix.${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundlePackageType APPL + CFBundleShortVersionString ${PROJECT_VERSION} + CFBundleVersion ${PROJECT_VERSION} + NSHumanReadableCopyright Copyright (c) 2008-${CURRENT_YEAR} the MRtrix3 contributors + LSMinimumSystemVersion ${CMAKE_OSX_DEPLOYMENT_TARGET} + LSBackgroundOnly 0 + NSHighResolutionCapable + + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + MRView file + CFBundleURLSchemes + + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + + + + + CFBundleDocumentTypes + + + + CFBundleTypeExtensions + + gz + + CFBundleTypeName + MRtrix of NIfTI image (compressed) + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + mif + + CFBundleTypeName + MRtrix image + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + mih + + CFBundleTypeName + MRtrix image header + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + mif.gz + + CFBundleTypeName + MRtrix image (compressed) + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + nii + + CFBundleTypeName + NIfTI image + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + nii.gz + + CFBundleTypeName + NIfTI image (compressed) + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + mgh + + CFBundleTypeName + MGH image + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + mgz + + CFBundleTypeName + MGH image (compressed) + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + CFBundleTypeExtensions + + img + + CFBundleTypeName + Analyze image + CFBundleTypeRole + Viewer + CFBundleTypeIconFile + ${MACOSX_BUNDLE_EXECUTABLE_NAME}_doc.icns + + + + + \ No newline at end of file diff --git a/cmake/bundle/shview.plist.in b/cmake/bundle/shview.plist.in new file mode 100644 index 0000000000..4dda4d4474 --- /dev/null +++ b/cmake/bundle/shview.plist.in @@ -0,0 +1,20 @@ + + + + + CFBundleInfoDictionaryVersion 6.0 + CFBundleDevelopmentRegion en + CFBundleName MRView + CFBundleDisplayName MRView + CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIconFile ${MACOSX_BUNDLE_EXECUTABLE_NAME}.icns + CFBundleIdentifier org.mrtrix.${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundlePackageType APPL + CFBundleShortVersionString ${PROJECT_VERSION} + CFBundleVersion ${PROJECT_VERSION} + NSHumanReadableCopyright Copyright (c) 2008-${CURRENT_YEAR} the MRtrix3 contributors + LSMinimumSystemVersion ${CMAKE_OSX_DEPLOYMENT_TARGET} + LSBackgroundOnly 0 + NSHighResolutionCapable + + \ No newline at end of file diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index 36a6ba44e4..afc701eeb9 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -16,11 +16,14 @@ function(add_cmd CMD_SRC IS_GUI) ) set_target_properties(${CMD_NAME} PROPERTIES MACOSX_BUNDLE ${IS_GUI} - MACOSX_BUNDLE_ICON_FILE mrview.icns + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/bundle/${CMD_NAME}.plist.in LINK_DEPENDS_NO_SHARED true RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin ) - install(TARGETS ${CMD_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(TARGETS ${CMD_NAME} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} + ) endfunction() From 1f846cfb2c2579e7739a56c20dce31b4d7ef6ec3 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Thu, 26 Oct 2023 11:16:38 +0200 Subject: [PATCH 031/182] Add macos icons --- icons/macos/mrview.icns | Bin 0 -> 232623 bytes icons/macos/mrview_doc.icns | Bin 0 -> 177431 bytes icons/macos/shview.icns | Bin 0 -> 232623 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 icons/macos/mrview.icns create mode 100644 icons/macos/mrview_doc.icns create mode 100644 icons/macos/shview.icns diff --git a/icons/macos/mrview.icns b/icons/macos/mrview.icns new file mode 100644 index 0000000000000000000000000000000000000000..542cf14c271ad43fd1aa3620eefbefe32727ac8a GIT binary patch literal 232623 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

gILN- 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%0DNpmL<0FaN`&ZnI#@qBw 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 literal 0 HcmV?d00001 diff --git a/icons/macos/mrview_doc.icns b/icons/macos/mrview_doc.icns new file mode 100644 index 0000000000000000000000000000000000000000..630beba173029de8540fd848adaa7a341f383c26 GIT binary patch literal 177431 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 !WtR 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

gILN- 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%0DNpmL<0FaN`&ZnI#@qBw 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 literal 0 HcmV?d00001 From 17ce4167f558300a14356b51810d9a42e5a7ca2e Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Thu, 26 Oct 2023 11:53:57 +0200 Subject: [PATCH 032/182] This should probably be defined when calling cmake instead --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1207d802ed..2bef0007ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(mrtrix3 VERSION 3.0.4) include(GNUInstallDirs) -set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15.0") string(TIMESTAMP CURRENT_YEAR "%Y") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") From dc81d103e582ca5650fed302a08e05466e967408 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Thu, 26 Oct 2023 11:54:21 +0200 Subject: [PATCH 033/182] Fix typo --- cmake/bundle/shview.plist.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/bundle/shview.plist.in b/cmake/bundle/shview.plist.in index 4dda4d4474..0004423b15 100644 --- a/cmake/bundle/shview.plist.in +++ b/cmake/bundle/shview.plist.in @@ -4,8 +4,8 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleDevelopmentRegion en - CFBundleName MRView - CFBundleDisplayName MRView + CFBundleName SHView + CFBundleDisplayName SHView CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleIconFile ${MACOSX_BUNDLE_EXECUTABLE_NAME}.icns CFBundleIdentifier org.mrtrix.${MACOSX_BUNDLE_EXECUTABLE_NAME} @@ -17,4 +17,4 @@ LSBackgroundOnly 0 NSHighResolutionCapable - \ No newline at end of file + From 659ebbf6d59be405434dfb41369c9e4325a22f5b Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Fri, 27 Oct 2023 11:28:05 +0200 Subject: [PATCH 034/182] Include application icons with app bundle --- cmd/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index afc701eeb9..ef0afd1b0d 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -14,9 +14,17 @@ function(add_cmd CMD_SRC IS_GUI) $,mrtrix::gui,mrtrix::headless> mrtrix::exec-version-lib ) + if (IS_GUI) + set(mrtrix_icon_macos "${CMAKE_SOURCE_DIR}/icons/macos/${CMD_NAME}.icns") + set_source_files_properties(${mrtrix_icon_macos} PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) + target_sources(${CMD_NAME} PRIVATE ${mrtrix_icon_macos}) + endif () set_target_properties(${CMD_NAME} PROPERTIES MACOSX_BUNDLE ${IS_GUI} MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/bundle/${CMD_NAME}.plist.in + MACOSX_BUNDLE_ICON_FILE "${CMD_NAME}.icns" LINK_DEPENDS_NO_SHARED true RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin ) From 15bb65f7d4af8788c13985d07dad6966e0e7a217 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Fri, 27 Oct 2023 11:35:06 +0200 Subject: [PATCH 035/182] Include file association icons with app bundle --- cmd/CMakeLists.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index ef0afd1b0d..b15836f071 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -20,11 +20,17 @@ function(add_cmd CMD_SRC IS_GUI) MACOSX_PACKAGE_LOCATION "Resources" ) target_sources(${CMD_NAME} PRIVATE ${mrtrix_icon_macos}) + if (${CMD_NAME} STREQUAL mrview) + set(mrtrix_icon_macos "${CMAKE_SOURCE_DIR}/icons/macos/${CMD_NAME}_doc.icns") + set_source_files_properties(${mrtrix_icon_macos} PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) + target_sources(${CMD_NAME} PRIVATE ${mrtrix_icon_macos}) + endif () endif () set_target_properties(${CMD_NAME} PROPERTIES MACOSX_BUNDLE ${IS_GUI} MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/bundle/${CMD_NAME}.plist.in - MACOSX_BUNDLE_ICON_FILE "${CMD_NAME}.icns" LINK_DEPENDS_NO_SHARED true RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin ) From 52f876946223133bf59f2ae1a1674c32e0b8ddb0 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Fri, 27 Oct 2023 11:50:05 +0200 Subject: [PATCH 036/182] Only add app bundle icons on macos --- cmd/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index b15836f071..9046d5832e 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -14,7 +14,7 @@ function(add_cmd CMD_SRC IS_GUI) $,mrtrix::gui,mrtrix::headless> mrtrix::exec-version-lib ) - if (IS_GUI) + if (IS_GUI AND (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")) set(mrtrix_icon_macos "${CMAKE_SOURCE_DIR}/icons/macos/${CMD_NAME}.icns") set_source_files_properties(${mrtrix_icon_macos} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources" From 7f27e89c70b94db1eaf87b6be667748336136845 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Tue, 30 Jan 2024 11:10:32 +1100 Subject: [PATCH 037/182] 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 038/182] 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 039/182] 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 040/182] 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 041/182] 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 042/182] 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 043/182] 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 044/182] 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 045/182] 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 046/182] 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 047/182] 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 048/182] 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 049/182] 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 050/182] 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 051/182] 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 052/182] 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 053/182] 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 054/182] 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 055/182] 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 056/182] 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 057/182] 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 058/182] 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 059/182] 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 060/182] 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 061/182] 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 062/182] 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 063/182] 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 064/182] 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 065/182] 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 066/182] 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 067/182] 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 068/182] 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 069/182] 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 f4587933c17ab719fda917260401542e4aac2a9e Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Thu, 9 May 2024 10:28:32 +0100 Subject: [PATCH 070/182] Fix compilation of big object files in MINGW debug builds --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3497888008..d28a117e80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,11 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git AND NOT EXISTS ${CMAKE_CURRENT_SOURCE endif() +# Allow compilation of big object of files in debug mode on MINGW +if(MINGW AND CMAKE_BUILD_TYPE MATCHES "Debug") + add_compile_options(-Wa,-mbig-obj) +endif() + add_compile_definitions( MRTRIX_BUILD_TYPE="${CMAKE_BUILD_TYPE}" $<$:MRTRIX_WINDOWS> From 65ba8562b59e5c3c9ee15374bd662ccbf43f5068 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Mon, 13 May 2024 21:28:38 +1000 Subject: [PATCH 071/182] 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 072/182] 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 073/182] 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 074/182] 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 075/182] 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 076/182] 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 077/182] 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 078/182] 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 079/182] 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 080/182] 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 081/182] 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 082/182] 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 083/182] 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 084/182] 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. From 2eefdcde4c641aa6d80050d09ffb561c5d883346 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Wed, 22 May 2024 13:13:49 +0100 Subject: [PATCH 085/182] Add missing windows.h include header Including gl_core_3_3.h should automatically include windows.h if APIENTRY is not defined, but some libraries (e.g. latest versions of Qt) define this macro without including windows.h in their public headers (to avoid polluting the global namespace). An explicit include is added to deal with this. --- src/gui/opengl/gl_core_3_3.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gui/opengl/gl_core_3_3.cpp b/src/gui/opengl/gl_core_3_3.cpp index 5514142d3d..2fa8767f66 100644 --- a/src/gui/opengl/gl_core_3_3.cpp +++ b/src/gui/opengl/gl_core_3_3.cpp @@ -14,6 +14,10 @@ * For more details, see http://www.mrtrix.org/. */ +#if defined(_WIN32) +#include +#endif + #include "gui/opengl/gl_core_3_3.h" #include #include From 961eeb2edb9894451e717ddbd793fdccd1b86b4f Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 23 May 2024 10:46:50 +1000 Subject: [PATCH 086/182] OpenGL: Minimise what is imported by windows.h --- src/gui/opengl/gl_core_3_3.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/gui/opengl/gl_core_3_3.cpp b/src/gui/opengl/gl_core_3_3.cpp index 2fa8767f66..884d7e702b 100644 --- a/src/gui/opengl/gl_core_3_3.cpp +++ b/src/gui/opengl/gl_core_3_3.cpp @@ -15,6 +15,13 @@ */ #if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOSERVICE +#define NOMCX +#define NOIME +#ifndef NOMINMAX +#define NOMINMAX +#endif #include #endif From 05d4fa56230548ce7e6f2f89f85c6988666321df Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Wed, 15 May 2024 13:15:04 +0100 Subject: [PATCH 087/182] New option to build non-core code as a static lib --- CMakeLists.txt | 1 + src/CMakeLists.txt | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3860e1cfa9..20266ccee6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ option(MRTRIX_STL_DEBUGGING "Enable STL debug mode" OFF) option(MRTRIX_BUILD_TESTS "Build tests executables" OFF) option(MRTRIX_STRIP_CONDA "Strip ananconda/mininconda from PATH to avoid conflicts" ON) option(MRTRIX_USE_PCH "Use precompiled headers" ON) +option(MRTRIX_BUILD_STATIC "Build MRtrix's non-core code as a static library" OFF) if(MRTRIX_BUILD_TESTS) if(CMAKE_VERSION VERSION_GREATER 3.17) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4802b59f9a..c5ffb6597a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,7 +40,13 @@ add_library(mrtrix-exec-version-lib STATIC ${EXEC_VERSION_CPP}) add_library(mrtrix::exec-version-lib ALIAS mrtrix-exec-version-lib) add_dependencies(mrtrix-exec-version-lib exec-version-target) -add_library(mrtrix-headless SHARED ${HEADLESS_SOURCES}) +if(MRTRIX_BUILD_STATIC) + set(MRTRIX_LIBRARY_TYPE STATIC) +else() + set(MRTRIX_LIBRARY_TYPE SHARED) +endif() + +add_library(mrtrix-headless ${MRTRIX_LIBRARY_TYPE} ${HEADLESS_SOURCES}) add_library(mrtrix::headless ALIAS mrtrix-headless) @@ -65,7 +71,7 @@ target_link_libraries(mrtrix-headless PUBLIC ) if(MRTRIX_BUILD_GUI) - add_library(mrtrix-gui SHARED ${GUI_SOURCES} ${RCC_SOURCES}) + add_library(mrtrix-gui ${MRTRIX_LIBRARY_TYPE} ${GUI_SOURCES} ${RCC_SOURCES}) add_library(mrtrix::gui ALIAS mrtrix-gui) set_target_properties(mrtrix-gui PROPERTIES From 7b0183193392693014b590f9577140b76c1c14c2 Mon Sep 17 00:00:00 2001 From: Daljit Date: Thu, 16 May 2024 14:26:05 +0100 Subject: [PATCH 088/182] Rename MRTRIX_BUILD_STATIC to MRTRIX_BUILD_NON_CORE_STATIC --- CMakeLists.txt | 2 +- src/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 20266ccee6..bb8069f1af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ option(MRTRIX_STL_DEBUGGING "Enable STL debug mode" OFF) option(MRTRIX_BUILD_TESTS "Build tests executables" OFF) option(MRTRIX_STRIP_CONDA "Strip ananconda/mininconda from PATH to avoid conflicts" ON) option(MRTRIX_USE_PCH "Use precompiled headers" ON) -option(MRTRIX_BUILD_STATIC "Build MRtrix's non-core code as a static library" OFF) +option(MRTRIX_BUILD_NON_CORE_STATIC "Build MRtrix's non-core code as a static library" OFF) if(MRTRIX_BUILD_TESTS) if(CMAKE_VERSION VERSION_GREATER 3.17) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c5ffb6597a..55c51f5ecd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,7 +40,7 @@ add_library(mrtrix-exec-version-lib STATIC ${EXEC_VERSION_CPP}) add_library(mrtrix::exec-version-lib ALIAS mrtrix-exec-version-lib) add_dependencies(mrtrix-exec-version-lib exec-version-target) -if(MRTRIX_BUILD_STATIC) +if(MRTRIX_BUILD_NON_CORE_STATIC) set(MRTRIX_LIBRARY_TYPE STATIC) else() set(MRTRIX_LIBRARY_TYPE SHARED) From 1a015ddd9e08b520e9966b8e49db76d93f1cbb72 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Fri, 31 May 2024 15:31:05 +0100 Subject: [PATCH 089/182] Move cmake/bundle to packaging/macos --- {cmake => packaging/macos}/bundle/mrview.plist.in | 0 {cmake => packaging/macos}/bundle/shview.plist.in | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {cmake => packaging/macos}/bundle/mrview.plist.in (100%) rename {cmake => packaging/macos}/bundle/shview.plist.in (100%) diff --git a/cmake/bundle/mrview.plist.in b/packaging/macos/bundle/mrview.plist.in similarity index 100% rename from cmake/bundle/mrview.plist.in rename to packaging/macos/bundle/mrview.plist.in diff --git a/cmake/bundle/shview.plist.in b/packaging/macos/bundle/shview.plist.in similarity index 100% rename from cmake/bundle/shview.plist.in rename to packaging/macos/bundle/shview.plist.in From 1eafc83a865eb832f299a8ed48f57bee0b334ad3 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Fri, 31 May 2024 15:34:14 +0100 Subject: [PATCH 090/182] Add MacOSBundle.cmake module --- CMakeLists.txt | 3 +++ cmake/MacOSBundle.cmake | 15 +++++++++++++++ cmd/CMakeLists.txt | 21 +++++---------------- 3 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 cmake/MacOSBundle.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 2926bf14f9..fd0c6cd78a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,9 @@ include(LinkerSetup) include(FindFFTW) include(CompilerCache) include(ECMEnableSanitizers) +if(CMAKE_SYSTEM_NAME MATCHES "Darwin") + include(MacOSBundle) +endif() use_compiler_cache() diff --git a/cmake/MacOSBundle.cmake b/cmake/MacOSBundle.cmake new file mode 100644 index 0000000000..87c54cbe7c --- /dev/null +++ b/cmake/MacOSBundle.cmake @@ -0,0 +1,15 @@ +function(set_bundle_properties executable_name) + if(${executable_name} STREQUAL "mrview") + set(mrtrix_icon_macos ${CMAKE_CURRENT_SOURCE_DIR}/../icons/macos/mrview_doc.icns) + else() + set(mrtrix_icon_macos ${CMAKE_CURRENT_SOURCE_DIR}/../icons/macos/${executable_name}.icns) + endif() + + target_sources(${executable_name} PRIVATE ${mrtrix_icon_macos}) + set_target_properties(${executable_name} PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/../packaging/macos/bundle/${executable_name}.plist.in" + RESOURCE ${mrtrix_icon_macos} + INSTALL_RPATH "@executable_path/../../../../lib" + ) +endfunction() diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index 67ec20ca00..5b5e712bb8 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -30,29 +30,18 @@ function(add_cmd CMD_SRC IS_GUI) $,mrtrix::gui,mrtrix::headless> mrtrix::exec-version-lib ) - if (IS_GUI AND (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")) - set(mrtrix_icon_macos "${CMAKE_SOURCE_DIR}/icons/macos/${CMD_NAME}.icns") - set_source_files_properties(${mrtrix_icon_macos} PROPERTIES - MACOSX_PACKAGE_LOCATION "Resources" - ) - target_sources(${CMD_NAME} PRIVATE ${mrtrix_icon_macos}) - if (${CMD_NAME} STREQUAL mrview) - set(mrtrix_icon_macos "${CMAKE_SOURCE_DIR}/icons/macos/${CMD_NAME}_doc.icns") - set_source_files_properties(${mrtrix_icon_macos} PROPERTIES - MACOSX_PACKAGE_LOCATION "Resources" - ) - target_sources(${CMD_NAME} PRIVATE ${mrtrix_icon_macos}) - endif () - endif () set_target_properties(${CMD_NAME} PROPERTIES - MACOSX_BUNDLE ${IS_GUI} - MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/bundle/${CMD_NAME}.plist.in LINK_DEPENDS_NO_SHARED true RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin ) if(MRTRIX_USE_PCH AND NOT ${IS_GUI}) target_precompile_headers(${CMD_NAME} REUSE_FROM pch_cmd) endif() + + if (IS_GUI AND ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + set_bundle_properties(${CMD_NAME}) + endif () + install(TARGETS ${CMD_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} From fc07064b4c050909cc8c0230366bd6eebbe2ddaf Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Thu, 6 Jun 2024 09:34:19 +1000 Subject: [PATCH 091/182] New commands: peaksconvert, peakscheck --- cmd/peaksconvert.cpp | 569 +++++++++++++++++++++++ docs/reference/commands/peakscheck.rst | 97 ++++ docs/reference/commands/peaksconvert.rst | 90 ++++ docs/reference/commands_list.rst | 4 + python/bin/peakscheck | 512 ++++++++++++++++++++ 5 files changed, 1272 insertions(+) create mode 100644 cmd/peaksconvert.cpp create mode 100644 docs/reference/commands/peakscheck.rst create mode 100644 docs/reference/commands/peaksconvert.rst create mode 100755 python/bin/peakscheck diff --git a/cmd/peaksconvert.cpp b/cmd/peaksconvert.cpp new file mode 100644 index 0000000000..91606f2eed --- /dev/null +++ b/cmd/peaksconvert.cpp @@ -0,0 +1,569 @@ +/* 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 "adapter/base.h" +#include "algo/loop.h" +#include "command.h" +#include "header.h" +#include "image.h" +#include "math/sphere.h" +#include "transform.h" + +using namespace MR; +using namespace App; + +// TODO Do we need to support both mathematics and physics conventions for spherical coordinates? +// And if so, where do we do it? + +const char *const formats[] = {"unitspherical", "spherical", "unit3vector", "3vector", nullptr}; +enum class format_t { UNITSPHERICAL, SPHERICAL, UNITTHREEVECTOR, THREEVECTOR }; +const char *const references[] = {"xyz", "ijk", "bvec", nullptr}; +enum class reference_t { XYZ, IJK, BVEC }; + +using transform_linear_type = Eigen::Matrix; + +// clang-format off +void usage() { + + AUTHOR = "Robert E. Smith (robert.smith@florey.edu.au)"; + + SYNOPSIS = "Convert peak directions images between formats and/or conventions"; + + DESCRIPTION + + "Under default operation with no command-line options specified, " + "the output image will be identical to the input image, " + "as the MRtrix convention (3-vectors defined with respect to RAS scanner space axes) " + "will be assumed to apply to both cases. " + "This behaviour is only modulated by explicitly providing command-line options " + "that give additional information about the format or convention of either image."; + + ARGUMENTS + + Argument ("input", "the input directions image").type_image_in() + + Argument ("output", "the output directions image").type_image_out(); + + OPTIONS + + OptionGroup ("Options providing information about the input image") + + Option ("in_format", "specify the format in which the input directions are specified") + + Argument("choice").type_choice(formats) + + Option ("in_reference", "specify the reference axes against which the input directions are specified" + " (assumed to be real / scanner space if omitted)") + + Argument("choice").type_choice(references) + // TODO Add option to import amplitudes to fuse with unit orientations / overwrite existing values + + + OptionGroup ("Options providing information about the output image") + + Option ("out_format", "specify the format in which the output directions will be specified" + " (will default to 3-vectors if omitted)") + + Argument("choice").type_choice(formats) + + Option ("out_reference", "specify the reference axes against which the output directions will be specified" + " (defaults to real / scanner space if omitted)") + + Argument("choice").type_choice(references) + // TODO Implement -fill + //+ Option ("fill", "specify value to be inserted into output image in the absence of valid information") + // + Argument("value").type_float(); + + // TODO Any additional options? + // - Reduce maximal number of fixels per voxel + +} +// clang-format on + +format_t format_from_option(const std::string &option_name) { + auto opt = get_options(option_name); + if (opt.empty()) + return format_t::THREEVECTOR; + switch (int(opt[0][0])) { + case 0: + return format_t::UNITSPHERICAL; + case 1: + return format_t::SPHERICAL; + case 2: + return format_t::UNITTHREEVECTOR; + case 3: + return format_t::THREEVECTOR; + default: + throw Exception("Unsupported input to option -" + option_name); + } +} +reference_t reference_from_option(const std::string &option_name) { + auto opt = get_options(option_name); + if (opt.empty()) + return reference_t::XYZ; + switch (int(opt[0][0])) { + case 0: + return reference_t::XYZ; + case 1: + return reference_t::IJK; + case 2: + return reference_t::BVEC; + default: + throw Exception("Unsupported input to option -" + option_name); + } +} +size_t volumes_per_fixel(format_t format) { return format == format_t::UNITSPHERICAL ? 2 : 3; } + +template class FormatBase { +public: + FormatBase(Eigen::Matrix) {} + virtual Eigen::Matrix operator()() const = 0; + static size_t num_elements() { return NumElements; } +}; + +class UnitSpherical : public FormatBase<2> { +public: + UnitSpherical(Eigen::Matrix in) : FormatBase(in), azimuth(in[0]), inclination(in[1]) {} + Eigen::Matrix operator()() const override { return {azimuth, inclination}; } + default_type azimuth, inclination; + friend std::ostream &operator<<(std::ostream &stream, const UnitSpherical &in) { + stream << "UnitSpherical(az=" << in.azimuth << ", in=" << in.inclination << ")"; + return stream; + } +}; + +class Spherical : public FormatBase<3> { +public: + Spherical(Eigen::Matrix in) : FormatBase(in), radius(in[0]), azimuth(in[1]), inclination(in[2]) {} + Eigen::Matrix operator()() const override { return {radius, azimuth, inclination}; } + default_type radius, azimuth, inclination; + friend std::ostream &operator<<(std::ostream &stream, const Spherical &in) { + stream << "Spherical(r=" << in.radius << ", az=" << in.azimuth << ", in=" << in.inclination << ")"; + return stream; + } +}; + +class UnitThreeVector : public FormatBase<3> { +public: + UnitThreeVector(Eigen::Matrix in) : FormatBase(in), unitthreevector(in) { + unitthreevector.normalize(); + } + Eigen::Matrix operator()() const override { return unitthreevector; } + Eigen::Vector3d unitthreevector; + friend std::ostream &operator<<(std::ostream &stream, const UnitThreeVector &in) { + stream << "UnitThreeVector(" << in.unitthreevector.transpose() << ")"; + return stream; + } +}; + +class ThreeVector : public FormatBase<3> { +public: + ThreeVector(Eigen::Matrix in) : FormatBase(in), threevector(in) {} + Eigen::Matrix operator()() const override { return threevector; } + Eigen::Matrix normalized() const { return threevector.normalized(); } + default_type radius() const { return threevector.norm(); } + Eigen::Vector3d threevector; + friend std::ostream &operator<<(std::ostream &stream, const ThreeVector &in) { + stream << "ThreeVector(" << in.threevector.transpose() << ")"; + return stream; + } +}; + +// Common intermediary format to be used regardless of input / output image details +// - ALWAYS in XYZ space +// - ALWAYS with a unit 3-vector +// - ALWAYS with a radius term present, even if it might be filled with unity +class Fixel { +public: + Fixel(const Eigen::Vector3d &unit_threevector_xyz, default_type radius) + : unit_threevector_xyz(unit_threevector_xyz), radius(radius) {} + Fixel(const Eigen::Vector3d &unit_threevector_xyz) + : unit_threevector_xyz(unit_threevector_xyz), radius(default_type(1)) {} + + template static Fixel from(const T &); + template T to() const; + + static void set_input_transforms(const Header &H); + static void set_output_transforms(const Header &H); + + friend std::ostream &operator<<(std::ostream &stream, const Fixel &in) { + stream << "Fixel([" << in.unit_threevector_xyz.transpose() << "]: " << in.radius << ")"; + return stream; + } + +private: + Eigen::Vector3d unit_threevector_xyz; + default_type radius; + + static transform_linear_type in_ijk2xyz; + static bool in_bvec_flipi; + static default_type in_bvec_imultiplier; + static Eigen::Vector3d in_bvec2ijk; + + static transform_linear_type out_ijk2xyz; + static transform_linear_type out_xyz2ijk; + static bool out_bvec_flipi; + static default_type out_bvec_imultiplier; + static Eigen::Vector3d out_ijk2bvec; +}; + +transform_linear_type Fixel::in_ijk2xyz = transform_linear_type::Constant(std::numeric_limits::signaling_NaN()); +bool Fixel::in_bvec_flipi = false; +default_type Fixel::in_bvec_imultiplier = std::numeric_limits::signaling_NaN(); +Eigen::Vector3d Fixel::in_bvec2ijk = Eigen::Vector3d::Constant(std::numeric_limits::signaling_NaN()); +transform_linear_type Fixel::out_ijk2xyz = transform_linear_type::Constant(std::numeric_limits::signaling_NaN()); +transform_linear_type Fixel::out_xyz2ijk = transform_linear_type::Constant(std::numeric_limits::signaling_NaN()); +bool Fixel::out_bvec_flipi = false; +default_type Fixel::out_bvec_imultiplier = std::numeric_limits::signaling_NaN(); +Eigen::Vector3d Fixel::out_ijk2bvec = Eigen::Vector3d::Constant(std::numeric_limits::signaling_NaN()); + +void Fixel::set_input_transforms(const Header &H) { + in_ijk2xyz = H.realignment().orig_transform().linear(); + in_bvec_flipi = in_ijk2xyz.determinant() > 0.0; + in_bvec_imultiplier = in_bvec_flipi ? -1.0 : 1.0; + in_bvec2ijk = {in_bvec_imultiplier, 1.0, 1.0}; + DEBUG("Input transform configured based on image \"" + H.name() + "\":"); + DEBUG("IJK-to-XYZ transform:\n" + str(in_ijk2xyz)); + DEBUG("bvec: flip " + str(in_bvec_flipi) + ", i component multiplier " + str(in_bvec_imultiplier) + ", vector multiplier [" + str(in_bvec2ijk.transpose()) + "]"); +} + +void Fixel::set_output_transforms(const Header &H) { + out_ijk2xyz = H.transform().linear(); + out_xyz2ijk = H.transform().inverse().linear(); + out_bvec_flipi = out_ijk2xyz.determinant() > 0.0; + out_bvec_imultiplier = out_bvec_flipi ? -1.0 : 1.0; + out_ijk2bvec = {out_bvec_imultiplier, 1.0, 1.0}; + DEBUG("Output transform configured based on image \"" + H.name() + "\":"); + DEBUG("IJK-to-XYZ transform:\n" + str(out_ijk2xyz)); + DEBUG("XYZ-to-IJK transform:\n" + str(out_xyz2ijk)); + DEBUG("bvec: flip " + str(out_bvec_flipi) + ", i component multiplier " + str(out_bvec_imultiplier) + ", vector multiplier [" + str(out_ijk2bvec.transpose()) + "]"); +} + +template <> Fixel Fixel::from(const UnitSpherical &in) { + const Eigen::Matrix az_in_xyz({in.azimuth, in.inclination}); + Eigen::Vector3d unit_threevector_xyz; + Math::Sphere::spherical2cartesian(az_in_xyz, unit_threevector_xyz); + return Fixel(unit_threevector_xyz); +} + +template <> Fixel Fixel::from(const UnitSpherical &in) { + const Eigen::Matrix az_in_ijk({in.azimuth, in.inclination}); + Eigen::Vector3d unit_threevector_ijk; + Math::Sphere::spherical2cartesian(az_in_ijk, unit_threevector_ijk); + return Fixel(in_ijk2xyz * unit_threevector_ijk); +} + +template <> Fixel Fixel::from(const UnitSpherical &in) { + const Eigen::Matrix az_in_bvec({in.azimuth, in.inclination}); + const Eigen::Matrix az_in_ijk( + {in_bvec_flipi ? Math::pi - az_in_bvec[0] : az_in_bvec[0], az_in_bvec[1]}); + Eigen::Vector3d unit_threevector_ijk; + Math::Sphere::spherical2cartesian(az_in_ijk, unit_threevector_ijk); + return Fixel(in_ijk2xyz * unit_threevector_ijk); +} + +template <> Fixel Fixel::from(const Spherical &in) { + const Eigen::Matrix r_az_in_xyz({in.radius, in.azimuth, in.inclination}); + Eigen::Vector3d unit_threevector_xyz; + Math::Sphere::spherical2cartesian(r_az_in_xyz.tail<2>(), unit_threevector_xyz); + return Fixel(unit_threevector_xyz, r_az_in_xyz[0]); +} + +template <> Fixel Fixel::from(const Spherical &in) { + const Eigen::Matrix r_az_in_ijk({in.radius, in.azimuth, in.inclination}); + Eigen::Vector3d unit_threevector_ijk; + Math::Sphere::spherical2cartesian(r_az_in_ijk.tail<2>(), unit_threevector_ijk); + return Fixel(in_ijk2xyz * unit_threevector_ijk, r_az_in_ijk[0]); +} + +template <> Fixel Fixel::from(const Spherical &in) { + const Eigen::Matrix r_az_in_bvec({in.radius, in.azimuth, in.inclination}); + const Eigen::Matrix r_az_in_ijk( + {r_az_in_bvec[0], in_bvec_flipi ? Math::pi - r_az_in_bvec[1] : r_az_in_bvec[1], r_az_in_bvec[2]}); + Eigen::Vector3d unit_threevector_ijk; + Math::Sphere::spherical2cartesian(r_az_in_ijk.tail<2>(), unit_threevector_ijk); + return Fixel(in_ijk2xyz * unit_threevector_ijk, r_az_in_ijk[0]); +} + +template <> Fixel Fixel::from(const UnitThreeVector &in) { + return Fixel(in()); +} + +template <> Fixel Fixel::from(const UnitThreeVector &in) { + return Fixel(in_ijk2xyz * in()); +} +template <> Fixel Fixel::from(const UnitThreeVector &in) { + return Fixel(in_ijk2xyz * (in().cwiseProduct(in_bvec2ijk))); +} + +template <> Fixel Fixel::from(const ThreeVector &in) { + return Fixel(in.normalized(), in.radius()); +} + +template <> Fixel Fixel::from(const ThreeVector &in) { + return Fixel(in_ijk2xyz * in.normalized(), in.radius()); +} + +template <> Fixel Fixel::from(const ThreeVector &in) { + return Fixel(in_ijk2xyz * (in.normalized().cwiseProduct(in_bvec2ijk)), in.radius()); +} + +template <> UnitSpherical Fixel::to() const { + Eigen::Matrix az_in_xyz; + Math::Sphere::cartesian2spherical(unit_threevector_xyz, az_in_xyz); + return UnitSpherical(az_in_xyz); +} + +template <> UnitSpherical Fixel::to() const { + const default_type azimuth = std::atan2(unit_threevector_xyz.dot(out_ijk2xyz.col(1)), + unit_threevector_xyz.dot(out_ijk2xyz.col(0))); + const default_type inclination = std::acos(unit_threevector_xyz.dot(out_ijk2xyz.col(2))); + return UnitSpherical({azimuth, inclination}); +} + +template <> UnitSpherical Fixel::to() const { + default_type azimuth = std::atan2(unit_threevector_xyz.dot(out_ijk2xyz.col(1)), + unit_threevector_xyz.dot(out_ijk2xyz.col(0))); + if (out_bvec_flipi) + azimuth = Math::pi - azimuth; + const default_type inclination = std::acos(unit_threevector_xyz.dot(out_ijk2xyz.col(2))); + return UnitSpherical({azimuth, inclination}); +} + +template <> Spherical Fixel::to() const { + Eigen::Matrix r_az_in_xyz; + r_az_in_xyz[0] = radius; + Math::Sphere::cartesian2spherical(unit_threevector_xyz, r_az_in_xyz.tail<2>()); + return Spherical(r_az_in_xyz); +} + +template <> Spherical Fixel::to() const { + const default_type azimuth = std::atan2(unit_threevector_xyz.dot(out_ijk2xyz.col(1)), + unit_threevector_xyz.dot(out_ijk2xyz.col(0))); + const default_type inclination = std::acos(unit_threevector_xyz.dot(out_ijk2xyz.col(2))); + return Spherical({radius, azimuth, inclination}); +} + +template <> Spherical Fixel::to() const { + default_type azimuth = std::atan2(unit_threevector_xyz.dot(out_ijk2xyz.col(1)), + unit_threevector_xyz.dot(out_ijk2xyz.col(0))); + if (out_bvec_flipi) + azimuth = Math::pi - azimuth; + const default_type inclination = std::acos(unit_threevector_xyz.dot(out_ijk2xyz.col(2))); + return Spherical({radius, azimuth, inclination}); +} + +template <> UnitThreeVector Fixel::to() const { + return UnitThreeVector(unit_threevector_xyz); +} + +template <> UnitThreeVector Fixel::to() const { + return UnitThreeVector(out_xyz2ijk * unit_threevector_xyz); +} + +template <> UnitThreeVector Fixel::to() const { + return UnitThreeVector((out_xyz2ijk * unit_threevector_xyz).cwiseProduct(out_ijk2bvec)); +} + +template <> ThreeVector Fixel::to() const { + return ThreeVector(unit_threevector_xyz * radius); +} + +template <> ThreeVector Fixel::to() const { + return ThreeVector(out_xyz2ijk * unit_threevector_xyz * radius); +} + +template <> ThreeVector Fixel::to() const { + return ThreeVector((out_xyz2ijk * unit_threevector_xyz).cwiseProduct(out_ijk2bvec) * radius); +} + +template class FixelImage : public Adapter::Base, Image> { +public: + using ImageType = Image; + using BaseType = Adapter::Base, ImageType>; + using BaseType::parent; + FixelImage(Image &that) : BaseType(that), fixel_index(0) {} + void reset() { + parent().reset(); + fixel_index = 0; + } + ssize_t size(size_t axis) const { + return axis == 3 ? (parent().size(3) / FixelType::num_elements()) : parent().size(axis); + } + ssize_t get_index(size_t axis) const { return (axis == 3) ? fixel_index : parent().get_index(axis); } + void move_index(size_t axis, ssize_t increment) { + if (axis != 3) { + parent().move_index(axis, increment); + return; + } + parent().move_index(3, FixelType::num_elements() * increment); + fixel_index += increment; + } + FixelType get_value() { + Eigen::Matrix data( + Eigen::Matrix::Zero(FixelType::num_elements())); + for (size_t index = 0; index != FixelType::num_elements(); ++index) { + data[index] = parent().get_value(); + parent().move_index(3, 1); + } + parent().move_index(3, -FixelType::num_elements()); + return FixelType(data); + } + void set_value(const FixelType &value) { + Eigen::Matrix data( + Eigen::Matrix::Zero(FixelType::num_elements())); + data = value(); + for (size_t index = 0; index != FixelType::num_elements(); ++index) { + parent().set_value(data[index]); + parent().move_index(3, 1); + } + parent().move_index(3, -FixelType::num_elements()); + } + +private: + ssize_t fixel_index; +}; + +template +void run(FixelImage &in_fixel_image, FixelImage &out_fixel_image) { + // TODO Multi-thread + // TODO Test to see if this naturally works across bootstrap realisations + for (auto l = Loop("Converting peaks orientations", in_fixel_image)(in_fixel_image, out_fixel_image); l; ++l) { + const Fixel fixel(Fixel::from(in_fixel_image.get_value())); + out_fixel_image.set_value(fixel.to()); + } +} + +template +void run(reference_t in_reference, FixelImage &in_fixel_image, FixelImage &out_fixel_image) { + switch (in_reference) { + case reference_t::XYZ: + run(in_fixel_image, out_fixel_image); + return; + case reference_t::IJK: + run(in_fixel_image, out_fixel_image); + return; + case reference_t::BVEC: + run(in_fixel_image, out_fixel_image); + return; + } +} + +template +void run(format_t in_format, + reference_t in_reference, + Image &input_image, + FixelImage &out_fixel_image) { + switch (in_format) { + case format_t::UNITSPHERICAL: { + FixelImage in_fixel_image(input_image); + run(in_reference, in_fixel_image, out_fixel_image); + return; + } + case format_t::SPHERICAL: { + FixelImage in_fixel_image(input_image); + run(in_reference, in_fixel_image, out_fixel_image); + return; + } + case format_t::UNITTHREEVECTOR: { + FixelImage in_fixel_image(input_image); + run(in_reference, in_fixel_image, out_fixel_image); + return; + } + case format_t::THREEVECTOR: { + FixelImage in_fixel_image(input_image); + run(in_reference, in_fixel_image, out_fixel_image); + return; + } + default: + assert(false); + } +} + +template +void run(format_t in_format, + reference_t in_reference, + Image &input_image, + reference_t out_reference, + FixelImage &out_fixel_image) { + switch (out_reference) { + case reference_t::XYZ: + run(in_format, in_reference, input_image, out_fixel_image); + return; + case reference_t::IJK: + run(in_format, in_reference, input_image, out_fixel_image); + return; + case reference_t::BVEC: + run(in_format, in_reference, input_image, out_fixel_image); + return; + } +} + +void run(format_t in_format, + reference_t in_reference, + Image &input_image, + format_t out_format, + reference_t out_reference, + Image &output_image) { + switch (out_format) { + case format_t::UNITSPHERICAL: { + FixelImage out_fixel_image(output_image); + run(in_format, in_reference, input_image, out_reference, out_fixel_image); + return; + } + case format_t::SPHERICAL: { + FixelImage out_fixel_image(output_image); + run(in_format, in_reference, input_image, out_reference, out_fixel_image); + return; + } + case format_t::UNITTHREEVECTOR: { + FixelImage out_fixel_image(output_image); + run(in_format, in_reference, input_image, out_reference, out_fixel_image); + return; + } + case format_t::THREEVECTOR: { + FixelImage out_fixel_image(output_image); + run(in_format, in_reference, input_image, out_reference, out_fixel_image); + return; + } + default: + assert(false); + } +} + +void run() { + + Header H_in = Header::open(argument[0]); + if (H_in.ndim() != 4) + throw Exception("Input image must be 4D"); + + const format_t in_format(format_from_option("in_format")); + const size_t in_volumes_per_fixel(volumes_per_fixel(in_format)); + const size_t num_fixels = H_in.size(3) / in_volumes_per_fixel; + if (num_fixels * in_volumes_per_fixel != H_in.size(3)) + throw Exception("Number of volumes in input image (" + str(H_in.size(3)) + ")" + + " incompatible with " + + str(volumes_per_fixel) + " volumes per orientation"); + const reference_t in_reference(reference_from_option("in_reference")); + + const format_t out_format(format_from_option("out_format")); + if ((in_format == format_t::SPHERICAL || in_format == format_t::THREEVECTOR) && + (out_format == format_t::UNITSPHERICAL || out_format == format_t::UNITTHREEVECTOR)) { + WARN("Output image will not include amplitudes that may be present in input image due to chosen format"); + } + const reference_t out_reference(reference_from_option("out_reference")); + + Header H_out(H_in); + H_out.name() = std::string(argument[1]); + H_out.size(3) = num_fixels * volumes_per_fixel(out_format); + Stride::set_from_command_line(H_out); + + Fixel::set_input_transforms(H_in); + Fixel::set_output_transforms(H_out); + + auto input = H_in.get_image(); + auto output = Image::create(argument[1], H_out); + run(in_format, in_reference, input, out_format, out_reference, output); +} diff --git a/docs/reference/commands/peakscheck.rst b/docs/reference/commands/peakscheck.rst new file mode 100644 index 0000000000..fc0bab6732 --- /dev/null +++ b/docs/reference/commands/peakscheck.rst @@ -0,0 +1,97 @@ +.. _peakscheck: + +peakscheck +========== + +Synopsis +-------- + +Check the orientations of an image containing discrete fibre orientations + +Usage +----- + +:: + + peakscheck input [ options ] + +- *input*: The input fibre orientations image to be checked + +Description +----------- + +MRtrix3 expects "peaks" images to be stored using the real / scanner space axes as reference. There are three possible sources of error in this interpretation: 1. There may be erroneous axis flips and/or permutations, but within the real / scanner space reference. 2. The image data may provide fibre orientations with reference to the image axes rather than real / scanner space. Here there are two additional possibilities: 2a. There may be requisite axis permutations / flips to be applied to the image data *before* transforming them to real / scanner space. 2b. There may be requisite axis permutations / flips to be applied to the image data *after* transforming them to real / scanner space. + +Options +------- + +- **-mask image** Provide a mask image within which to seed & constrain tracking + +- **-number** Set the number of tracks to generate for each test + +- **-threshold** Modulate thresold on the ratio of empirical to maximal mean length to issue an error + +- **-in_format** The format in which peak orientations are specified; one of: spherical,unitspherical,3vector,unit3vector + +- **-noshuffle** Do not evaluate possibility of requiring shuffles of axes or angles; only consider prospective transforms from alternative reference frames to real / scanner space + +- **-notransform** Do not evaluate possibility of requiring transform of peak orientations from image to real / scanner space; only consider prospective shuffles of axes or angles + +- **-all** Print table containing all results to standard output + +- **-out_table** Write text file with table containing all results + +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 ** 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/docs/reference/commands/peaksconvert.rst b/docs/reference/commands/peaksconvert.rst new file mode 100644 index 0000000000..b5d9c2702c --- /dev/null +++ b/docs/reference/commands/peaksconvert.rst @@ -0,0 +1,90 @@ +.. _peaksconvert: + +peaksconvert +=================== + +Synopsis +-------- + +Convert peak directions images between formats and/or conventions + +Usage +-------- + +:: + + peaksconvert [ options ] input output + +- *input*: the input directions image +- *output*: the output directions image + +Description +----------- + +Under default operation with no command-line options specified, the output image will be identical to the input image, as the MRtrix convention (3-vectors defined with respect to RAS scanner space axes) will be assumed to apply to both cases. This behaviour is only modulated by explicitly providing command-line options that give additional information about the format or convention of either image. + +Options +------- + +Options providing information about the input image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **-in_format choice** specify the format in which the input directions are specified + +- **-in_reference choice** specify the reference axes against which the input directions are specified (assumed to be real / scanner space if omitted) + +Options providing information about the output image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **-out_format choice** specify the format in which the output directions will be specified (will default to 3-vectors if omitted) + +- **-out_reference choice** specify the reference axes against which the output directions will be specified (defaults to real / scanner space if omitted) + +- **-fill value** specify value to be inserted into output image in the absence of valid information + +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/docs/reference/commands_list.rst b/docs/reference/commands_list.rst index c29f39b65d..5a6334c470 100644 --- a/docs/reference/commands_list.rst +++ b/docs/reference/commands_list.rst @@ -96,6 +96,8 @@ List of MRtrix3 commands commands/mtnormalise commands/peaks2amp commands/peaks2fixel + commands/peakscheck + commands/peaksconvert commands/population_template commands/responsemean commands/sh2amp @@ -228,6 +230,8 @@ List of MRtrix3 commands |cpp.png|, :ref:`mtnormalise`, "Multi-tissue informed log-domain intensity normalisation" |cpp.png|, :ref:`peaks2amp`, "Extract amplitudes from a peak directions image" |cpp.png|, :ref:`peaks2fixel`, "Convert peak directions image to a fixel directory" + |python.png|, :ref:`peakscheck`, "Check the orientations of an image containing discrete fibre orientations" + |cpp.png|, :ref:`peaksconvert`, "Convert peak directions images between formats and/or conventions" |python.png|, :ref:`population_template`, "Generates an unbiased group-average template from a series of images" |python.png|, :ref:`responsemean`, "Calculate the mean response function from a set of text files" |cpp.png|, :ref:`sh2amp`, "Evaluate the amplitude of an image of spherical harmonic functions along specified directions" diff --git a/python/bin/peakscheck b/python/bin/peakscheck new file mode 100755 index 0000000000..4843b7db1e --- /dev/null +++ b/python/bin/peakscheck @@ -0,0 +1,512 @@ +#!/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/. + +import itertools +import os +import sys + +from mrtrix3 import MRtrixError #pylint: disable=no-name-in-module +from mrtrix3 import app, image, path, run #pylint: disable=no-name-in-module + + + +EUCLIDEAN_FLIPS = (None, 0, 1, 2) +EUCLIDEAN_PERMUTATIONS = ((0,1,2), (0,2,1), (1,0,2), (1,2,0), (2,0,1), (2,1,0)) + +THREEVECTOR_OPERATION_SETS = ([], + ['euclidean_shuffle'], + ['euclidean_transform'], + ['euclidean_shuffle', 'euclidean_transform'], + ['euclidean_transform', 'euclidean_shuffle']) + +# - 'convert' here refers specifically to a spherical to cartesian conversion +# - TODO For now, going to have all possible combinations of "transform" and "convert"; +# while these could ideally be combined in a single step in many instances, +# as a first pass it may be preferable to just evaluate all possibilities with individualised operations +SPHERICAL_OPERATION_SETS = (['convert'], + ['convert', 'euclidean_shuffle'], + ['convert', 'euclidean_transform'], + ['spherical_shuffle', 'convert'], + ['spherical_transform', 'convert'], + ['convert', 'euclidean_shuffle', 'euclidean_transform'], + ['convert', 'euclidean_transform', 'euclidean_shuffle'], + ['spherical_shuffle', 'convert', 'euclidean_transform'], + ['spherical_shuffle', 'spherical_transform', 'convert'], + ['spherical_transform', 'convert', 'euclidean_shuffle'], + ['spherical_transform', 'spherical_shuffle', 'convert']) + +# TODO Current framework may omit situation where an XYZ2 transform has been erroneously applied, +# which would therefore necessitate *two* 2XYZ transforms to be applied to get into XYZ reference space + +SPHERICAL_SWAPS = (False, True) +SPHERICAL_EL2INC = (False, True) + +# TODO Ideally, rather than considering these two as independent, +# only do one; +# but if bvec and ijk are equivalent, +# or if a combination of ijk transform and flipping first element is performed, +# then variant names should reflect this +INPUT_REFERENCES = ('bvec', 'ijk') + +DEFAULT_NUMBER = 10000 +DEFAULT_THRESHOLD = 0.95 + +FORMATS = ('spherical', 'unitspherical', '3vector', 'unit3vector') +DEFAULT_FORMAT = '3vector' + + + +def usage(cmdline): #pylint: disable=unused-variable + cmdline.set_author('Robert E. Smith (robert.smith@florey.edu.au)') + cmdline.set_synopsis('Check the orientations of an image containing discrete fibre orientations') + cmdline.add_description('MRtrix3 expects "peaks" images to be stored' + ' using the real / scanner space axes as reference.' + ' There are three possible sources of error in this interpretation:' + ' 1. There may be erroneous axis flips and/or permutations,' + ' but within the real / scanner space reference.' + ' 2. The image data may provide fibre orientations' + ' with reference to the image axes rather than real / scanner space.' + ' Here there are two additional possibilities:' + ' 2a. There may be requisite axis permutations / flips' + ' to be applied to the image data *before* transforming them to real / scanner space.' + ' 2b. There may be requisite axis permutations / flips' + ' to be applied to the image data *after* transforming them to real / scanner space.') + cmdline.add_argument('input', help='The input fibre orientations image to be checked') + cmdline.add_argument('-mask', metavar='image', help='Provide a mask image within which to seed & constrain tracking') + cmdline.add_argument('-number', type=int, default=DEFAULT_NUMBER, help='Set the number of tracks to generate for each test') + cmdline.add_argument('-threshold', type=float, default=DEFAULT_THRESHOLD, help='Modulate thresold on the ratio of empirical to maximal mean length to issue an error') + cmdline.add_argument('-in_format', choices=FORMATS, default=DEFAULT_FORMAT, help='The format in which peak orientations are specified; one of: ' + ','.join(FORMATS)) + # TODO Add -in_reference option, which would control which variant is considered the default for the purpose of command return code + cmdline.add_argument('-noshuffle', + action='store_true', + help='Do not evaluate possibility of requiring shuffles of axes or angles;' + ' only consider prospective transforms from alternative reference frames to real / scanner space') + cmdline.add_argument('-notransform', + action='store_true', + help='Do not evaluate possibility of requiring transform of peak orientations from image to real / scanner space;' + ' only consider prospective shuffles of axes or angles') + cmdline.flag_mutually_exclusive_options(['noshuffle', 'notransform']) + cmdline.add_argument('-all', action='store_true', help='Print table containing all results to standard output') + cmdline.add_argument('-out_table', help='Write text file with table containing all results') + + + +class Operation: + @staticmethod + def prettytype(): + assert False + def prettyparams(self): + assert False + def cmd(self, nfixels, in_path, out_path): + assert False + +class EuclideanShuffle(Operation): + @staticmethod + def prettytype(): + return 'Euclidean axis shuffle' + def __init__(self, flip, permutations): + self.flip = flip + self.permutations = permutations + def __format__(self, fmt): + if self.no_flip(): + assert not self.no_permutations() + return f'perm{"".join(map(str, self.permutations))}' + if self.no_permutations(): + return f'flip{self.flip}' + return f'flip{self.flip}perm{"".join(map(str, self.permutations))}' + def prettyparams(self): + result = '' + if self.flip is not None: + result = f'Flip axis {self.flip}' + if not self.no_permutations(): + result += '; ' + if not self.no_permutations(): + result += f'Permute axes: ({",".join(map(str, self.permutations))})' + return result + def no_flip(self): + return self.flip is None + def no_permutations(self): + return self.permutations == (0,1,2) + def cmd(self, nfixels, in_path, out_path): + # Below is modified copy-paste from code prior to "new variants" + volume_list = [ None ] * (3 * nfixels) + for in_volume_index in range(0, 3*nfixels): + fixel_index = in_volume_index // 3 + in_component = in_volume_index - 3*fixel_index + # Do we need to invert this item prior to permutation? + flip = self.flip == in_component + flip_string = 'flip' if flip else '' + # What should be the index of this image after permutation has taken place? + out_component = self.permutations[in_component] + # Where will this volume reside in the output image series? + out_volume_index = 3*fixel_index + out_component + assert volume_list[out_volume_index] is None + # Create the image + temppath = f'{os.path.splitext(in_path)[0]}_{flip_string}{in_volume_index}_{out_volume_index}.mif' + cmd = ['mrconvert', in_path, + '-coord', '3', f'{in_volume_index}', + '-axes', '0,1,2', + '-config', 'RealignTransform', 'false'] + if flip: + cmd.extend(['-', '|', 'mrcalc', '-', '-1.0', '-mult', + '-config', 'RealignTransform', 'false']) + cmd.append(temppath) + run.command(cmd) + volume_list[out_volume_index] = temppath + assert all(item is not None for item in volume_list) + run.command(['mrcat', volume_list, out_path, + '-axis', '3', + '-config', 'RealignTransform', 'false']) + for item in volume_list: + os.remove(item) + +class EuclideanTransform(Operation): + @staticmethod + def prettytype(): + return 'Euclidean reference axis change' + def __init__(self, reference): + self.reference = reference + def __format__(self, fmt): + return f'euctrans{self.reference}2xyz' + def prettyparams(self): + return f'{self.reference} to xyz' + def cmd(self, nfixels, in_path, out_path): + run.command(['peaksconvert', in_path, out_path, + '-in_reference', self.reference, + '-in_format', '3vector', + '-out_reference', 'xyz', + '-out_format', '3vector', + '-config', 'RealignTransform', 'false']) + +class SphericalShuffle(Operation): + @staticmethod + def prettytype(): + return 'Spherical angle shuffle' + def __init__(self, is_unit, swap, el2in): + self.is_unit = is_unit + self.swap = swap + self.el2in = el2in + def __format__(self, fmt): + if self.swap: + if self.el2in: + return 'swapandel2in' + return 'swap' + return 'el2in' + def prettyparams(self): + result = '' + if self.swap: + result = 'Swap angles' + if self.el2in: + result += '; ' + if self.el2in: + result += ' Transform elevation to inclination' + return result + def volsperfixel(self): + return 2 if self.is_unit else 3 + def cmd(self, nfixels, in_path, out_path): + # Below is modified copy-paste from code prior to "new variants" + num_volumes = self.volsperfixel() * nfixels + volume_list = [ None ] * num_volumes + for in_volume_index in range(0, num_volumes): + fixel_index = in_volume_index // self.volsperfixel() + in_component = in_volume_index - (self.volsperfixel() * fixel_index) + # Is this a spherical angle that needs to be permuted? + if self.swap and not (not self.is_unit and in_component == 0): + # If unit, 0->1 and 1->0 + # If not unit, 1->2 and 2->1 + out_component = (1 if self.is_unit else 3) - in_component + else: + out_component = in_component + # Where will this volume reside in the output image series? + out_volume_index = 3*fixel_index + out_component + assert volume_list[out_volume_index] is None + # Create the image + temppath = f'{os.path.splitext(in_path)[0]}_{in_volume_index}{"el2in" if self.el2in else ""}_{out_volume_index}.mif' + cmd = ['mrconvert', in_path, + '-coord', '3', f'{in_volume_index}', + '-axes', '0,1,2', + '-config', 'RealignTransform', 'false'] + # Do we need to apply the elevation->inclination transform? + if self.el2in and out_component == self.volsperfixel() - 1: + cmd.extend(['-', '|', 'mrcalc', '0.5', 'pi', '-mult', '-', '-sub', + '-config', 'RealignTransform', 'false']) + cmd.append(temppath) + run.command(cmd) + volume_list[out_volume_index] = temppath + assert all(item is not None for item in volume_list) + run.command(['mrcat', volume_list, out_path, + '-axis', '3', + '-config', 'RealignTransform', 'false']) + for item in volume_list: + os.remove(item) + +class SphericalTransform(Operation): + @staticmethod + def prettytype(): + return 'Spherical reference axis change' + def __init__(self, is_unit, reference): + self.is_unit = is_unit + self.reference = reference + def __format__(self, fmt): + return f'sphtrans{self.reference}2xyz' + def prettyparams(self): + return f'{self.reference} to xyz' + def cmd(self, nfixels, in_path, out_path): + run.command(['peaksconvert', in_path, out_path, + '-in_reference', self.reference, + '-in_format', 'unitspherical' if self.is_unit else 'spherical', + '-out_reference', 'xyz', + '-out_format', 'unitspherical' if self.is_unit else 'spherical', + '-config', 'RealignTransform', 'false']) + +class Spherical2Cartesian(Operation): + @staticmethod + def prettytype(): + return 'Spherical-to-cartesian transformation' + def __init__(self, is_unit): + self.is_unit = is_unit + def __format__(self, fmt): + return 'sph2cart' + def prettyparams(self): + return 'N/A' + def cmd(self, nfixels, in_path, out_path): + run.command(['peaksconvert', in_path, out_path, + '-in_format', 'unitspherical' if self.is_unit else 'spherical', + '-out_format', 'unit3vector' if self.is_unit else '3vector', + '-config', 'RealignTransform', 'false']) + + + +class Variant(): + def __init__(self, operations): + assert isinstance(operations, list) + assert all(isinstance(item, Operation) for item in operations) + self.operations = operations + def __format__(self, fmt): + if not self.operations: + return 'none' + return '_'.join(f'{item}' for item in self.operations) + def is_default(self): + if not self.operations: + return True + return len(self.operations) == 1 and isinstance(self.operations[0], Spherical2Cartesian) + +def sort_key(item): + assert item.mean_length is not None + return item.mean_length + + + +def execute(): #pylint: disable=unused-variable + + app.check_output_path(app.ARGS.out_table) + + image_dimensions = image.Header(path.from_user(app.ARGS.input, False)).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] + if app.ARGS.in_format in ('unit3vector', '3vector'): + num_fixels = num_volumes // 3 + if 3 * num_fixels != num_volumes: + raise MRtrixError(f'Number of input volumes ({num_volumes}) not a valid peaks image:' + ' must be a multiple of 3') + elif app.ARGS.in_format == 'spherical': + num_fixels = num_volumes // 3 + if 3 * num_fixels != num_volumes: + raise MRtrixError(f'Number of input volumes ({num_volumes}) not a valid spherical coordinates image:' + ' must be a multiple of 3') + elif app.ARGS.in_format == 'unitspherical': + num_fixels = num_volumes // 2 + if 2 * num_fixels != num_volumes: + raise MRtrixError(f'Number of input volumes ({num_volumes}) not a valid unit spherical coordinates image:' + ' must be a multiple of 2') + else: + assert False + + is_unit = app.ARGS.in_format in ('unitspherical', 'unit3vector') + + app.make_scratch_dir() + + # Unlike dwigradcheck, here we're going to be performing manual permutation & flipping of volumes + # Therefore, we'd actually prefer to *not* have contiguous memory across volumes + # Also, in order for subsequent reference transforms to be valid, + # we need for the strides to not be modified by MRtrix3 at the load stage + run.command('mrconvert ' + path.from_user(app.ARGS.input) + ' ' + path.to_scratch('data.mif') + + ' -datatype float32' + + ' -config RealignTransform false') + + if app.ARGS.mask: + run.command('mrconvert ' + path.from_user(app.ARGS.mask) + ' ' + path.to_scratch('mask.mif') + + ' -datatype bit' + + ' -config RealignTransform false') + + app.goto_scratch_dir() + + # Generate a brain mask if we weren't provided with one + if not os.path.exists('mask.mif'): + run.command('mrcalc data.mif -abs - -config RealignTransform false | ' + 'mrmath - max -axis 3 - -config RealignTransform false | ' + 'mrthreshold - mask.mif -abs 0.0 -comparison gt -config RealignTransform false') + + # How many tracks are we going to generate? + number_option = ['-select', str(app.ARGS.number)] + + operation_sets = THREEVECTOR_OPERATION_SETS \ + if app.ARGS.in_format in ('unit3vector', '3vector') \ + else SPHERICAL_OPERATION_SETS + + # To facilitate looping in order to generate all possible variants, + # pre-prepare lists of all possible configurations of each operation + all_euclidean_shuffles = [] + for flip in EUCLIDEAN_FLIPS: + for permutation in EUCLIDEAN_PERMUTATIONS: + if flip is None and permutation == (0,1,2): + continue + all_euclidean_shuffles.append(EuclideanShuffle(flip, permutation)) + all_euclidean_transforms = [EuclideanTransform('ijk'), + EuclideanTransform('bvec')] + all_spherical_shuffles = [SphericalShuffle(is_unit, True, False), + SphericalShuffle(is_unit, False, True), + SphericalShuffle(is_unit, True, True)] + all_spherical_transforms = [SphericalTransform(is_unit, 'ijk'), + SphericalTransform(is_unit, 'bvec')] + all_spherical2cartesian = [Spherical2Cartesian(is_unit),] + + # TODO Add capabilities to restrict set of variants to be evaluated + variants = [] + for operation_set in operation_sets: + if app.ARGS.noshuffle and any('shuffle' in item for item in operation_set): + continue + if app.ARGS.notransform and any('transform' in item for item in operation_set): + continue + operations_list = [] + for operation in operation_set: + if operation == 'convert': + operations_list.append(all_spherical2cartesian) + elif operation == 'euclidean_shuffle': + operations_list.append(all_euclidean_shuffles) + elif operation == 'euclidean_transform': + operations_list.append(all_euclidean_transforms) + elif operation == 'spherical_shuffle': + operations_list.append(all_spherical_shuffles) + elif operation == 'spherical_transform': + operations_list.append(all_spherical_transforms) + else: + assert False + for variant in itertools.product(*operations_list): + variants.append(Variant(list(variant))) + app.debug(f'Complete list of variants for {app.ARGS.in_format} input format:') + for v in variants: + app.debug(f'{v}') + + progress = app.ProgressBar(f'Testing peaks orientation alterations (0 of {len(variants)})', len(variants)) + meanlength_default = None + for variant_index, variant in enumerate(variants): + + # For each variant, we now have to construct and execute the sequential set of commands necessary + tmppath = None + imagepath = 'data.mif' + for operation_index, operation in enumerate(variant.operations): + in_path = 'data.mif' if operation_index == 0 else tmppath + imagepath = f'{os.path.splitext(in_path)[0]}_{operation}.mif' + # Ensure that an intermediate output of one variant doesn't clash + # with the final output of another variant + if operation_index == len(variant.operations) - 1: + imagepath = imagepath[len('data_'):] + operation.cmd(num_fixels, in_path, imagepath) + if tmppath: + run.function(os.remove, tmppath) + tmppath = imagepath + + # Run the tracking experiment + track_file_path = f'tracks_{variant}.tck' + run.command(['tckgen', imagepath, + '-algorithm', 'fact', + '-seed_image', 'mask.mif', + '-mask', 'mask.mif', + '-minlength', '0', + '-downsample', '5', + '-config', 'RealignTransform', 'false', + track_file_path] + + number_option) + # TODO Would prefer nicer logic + if imagepath != 'data.mif': + app.cleanup(imagepath) + + # Get the mean track length & add to the database + mean_length = float(run.command(['tckstats', track_file_path, '-output', 'mean', '-ignorezero']).stdout) + variants[variant_index].mean_length = mean_length + app.cleanup(track_file_path) + + # Save result if this is the unmodified empirical gradient table + if variant.is_default(): + assert meanlength_default is None + meanlength_default = mean_length + + # Increment the progress bar + progress.increment(f'Testing peaks orientation alterations ({variant_index+1} of {len(variants)})') + + progress.done() + + # Sort the list to find the best gradient configuration(s) + sorted_variants = list(variants) + sorted_variants.sort(reverse=True, key=sort_key) + meanlength_max = sorted_variants[0].mean_length + + if sorted_variants[0].is_default(): + meanlength_ratio = 1.0 + app.console('Absence of manipulation of peaks orientations resulted in the maximal mean length') + else: + meanlength_ratio = meanlength_default / meanlength_max + app.console(f'Ratio of mean length of empirical data to mean length of best candidate: {meanlength_ratio:.3f}') + + # Provide a printout of the mean streamline length of each orientation manipulation + if app.ARGS.all: + sys.stdout.write('Mean length Variant\n') + for variant in sorted_variants: + sys.stdout.write(f' {variant.mean_length:5.2f} {variant}\n') + sys.stdout.flush() + + # Write comprehensive results to a file + if app.ARGS.out_table: + if os.path.splitext(app.ARGS.out_table)[-1].lower() == '.tsv': + delimiter = '\t' + quote = '' + else: + delimiter = ',' + quote = '"' + with open(path.from_user(app.ARGS.out_table, False), 'w') as f: + f.write(f'{quote}Mean length{quote}{delimiter}' + f'{quote}Operation 1 type{quote}{delimiter}{quote}Operation 1 parameters{quote}{delimiter}' + f'{quote}Operation 2 type{quote}{delimiter}{quote}Operation 2 parameters{quote}{delimiter}' + f'{quote}Operation 3 type{quote}{delimiter}{quote}Operation 3 parameters{quote}{delimiter}\n') + for v in variants: + f.write(f'{v.mean_length}') + for operation in v.operations: + f.write(f'{delimiter}{quote}{operation.prettytype()}{quote}{delimiter}{quote}{operation.prettyparams()}{quote}') + f.write('\n') + + if meanlength_ratio < app.ARGS.threshold: + raise MRtrixError('Streamline tractography indicates possibly incorrect gradient table') + + + +# Execute the script +import mrtrix3 #pylint: disable=wrong-import-position +mrtrix3.execute() #pylint: disable=no-member \ No newline at end of file From 419ab8f2958157a601f969ea14ee99015fc41b88 Mon Sep 17 00:00:00 2001 From: Daljit Date: Thu, 6 Jun 2024 10:14:53 +0100 Subject: [PATCH 092/182] Add warning if cmake generator is not Ninja --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 14d49d8979..6617d4cc3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,12 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(mrtrix3 LANGUAGES CXX VERSION 3.0.4) +if(NOT CMAKE_GENERATOR STREQUAL "Ninja") + message(WARNING "It is recommended to use the Ninja generator to build MRtrix3. " + "To use it, run cmake with -G Ninja or set the CMAKE_GENERATOR" + "environment variable to Ninja.") +endif() + include(GNUInstallDirs) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") From a805ef3b6cd3ab498bc3368542e4660e2f1afc52 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Tue, 4 Jun 2024 17:35:07 +0100 Subject: [PATCH 093/182] Create wrapper scripts for mrview and shview --- cmake/MacOSBundle.cmake | 14 ++++++++++++++ cmd/CMakeLists.txt | 1 + packaging/macos/bundle/wrapper_launcher.sh.in | 12 ++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 packaging/macos/bundle/wrapper_launcher.sh.in diff --git a/cmake/MacOSBundle.cmake b/cmake/MacOSBundle.cmake index 87c54cbe7c..c803390337 100644 --- a/cmake/MacOSBundle.cmake +++ b/cmake/MacOSBundle.cmake @@ -13,3 +13,17 @@ function(set_bundle_properties executable_name) INSTALL_RPATH "@executable_path/../../../../lib" ) endfunction() + +function(install_bundle_wrapper_scripts executable_name) + set(wrapper_script ${CMAKE_CURRENT_SOURCE_DIR}/../packaging/macos/bundle/wrapper_launcher.sh.in) + configure_file(${wrapper_script} ${PROJECT_BINARY_DIR}/bin/${executable_name} + FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + @ONLY + ) + + install(FILES ${PROJECT_BINARY_DIR}/bin/${executable_name} + DESTINATION ${CMAKE_INSTALL_BINDIR} + RENAME "${executable_name}" + PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) + +endfunction() diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index 5b5e712bb8..fd6de1b333 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -40,6 +40,7 @@ function(add_cmd CMD_SRC IS_GUI) if (IS_GUI AND ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") set_bundle_properties(${CMD_NAME}) + install_bundle_wrapper_scripts(${CMD_NAME}) endif () install(TARGETS ${CMD_NAME} diff --git a/packaging/macos/bundle/wrapper_launcher.sh.in b/packaging/macos/bundle/wrapper_launcher.sh.in new file mode 100644 index 0000000000..6702fcdd61 --- /dev/null +++ b/packaging/macos/bundle/wrapper_launcher.sh.in @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# This script is a wrapper around the actual MacOSX bundle executable. + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +real_path=${DIR}/@executable_name@.app/Contents/MacOS/@executable_name@ +if [ -x "${real_path}" ]; then + exec "${real_path}" "$@" +else + echo "Could not find executable at ${real_path}" + exit 1 +fi From b053f7c44f83ff1cd04071dde95eec73e8eb3805 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Tue, 4 Jun 2024 19:01:49 +0100 Subject: [PATCH 094/182] Set CMAKE_OSX_DEPLOYMENT_TARGET for MacOS This must be called prior to the project() --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index fd0c6cd78a..039d79b173 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) +set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "") project(mrtrix3 LANGUAGES CXX VERSION 3.0.4) include(GNUInstallDirs) From f4bee4173babfed410100f399128d016be1fbd39 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Tue, 4 Jun 2024 21:45:27 +0100 Subject: [PATCH 095/182] Set copyright year in MacOSBundle.cmake --- CMakeLists.txt | 2 -- cmake/MacOSBundle.cmake | 3 +++ packaging/macos/bundle/mrview.plist.in | 4 ++-- packaging/macos/bundle/shview.plist.in | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 039d79b173..04f5252c7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,6 @@ project(mrtrix3 LANGUAGES CXX VERSION 3.0.4) include(GNUInstallDirs) -string(TIMESTAMP CURRENT_YEAR "%Y") - list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") set(MRTRIX_BASE_VERSION "${CMAKE_PROJECT_VERSION}") diff --git a/cmake/MacOSBundle.cmake b/cmake/MacOSBundle.cmake index c803390337..0bb0fcde20 100644 --- a/cmake/MacOSBundle.cmake +++ b/cmake/MacOSBundle.cmake @@ -5,6 +5,9 @@ function(set_bundle_properties executable_name) set(mrtrix_icon_macos ${CMAKE_CURRENT_SOURCE_DIR}/../icons/macos/${executable_name}.icns) endif() + string(TIMESTAMP CURRENT_YEAR "%Y") + set(COPYRIGHT_YEAR "2008-${CURRENT_YEAR}" CACHE STRING "Copyright year") + target_sources(${executable_name} PRIVATE ${mrtrix_icon_macos}) set_target_properties(${executable_name} PROPERTIES MACOSX_BUNDLE TRUE diff --git a/packaging/macos/bundle/mrview.plist.in b/packaging/macos/bundle/mrview.plist.in index af08829e1d..169c12d49d 100644 --- a/packaging/macos/bundle/mrview.plist.in +++ b/packaging/macos/bundle/mrview.plist.in @@ -12,7 +12,7 @@ CFBundlePackageType APPL CFBundleShortVersionString ${PROJECT_VERSION} CFBundleVersion ${PROJECT_VERSION} - NSHumanReadableCopyright Copyright (c) 2008-${CURRENT_YEAR} the MRtrix3 contributors + NSHumanReadableCopyright Copyright (c) ${COPYRIGHT_YEAR} the MRtrix3 contributors LSMinimumSystemVersion ${CMAKE_OSX_DEPLOYMENT_TARGET} LSBackgroundOnly 0 NSHighResolutionCapable @@ -153,4 +153,4 @@ - \ No newline at end of file + diff --git a/packaging/macos/bundle/shview.plist.in b/packaging/macos/bundle/shview.plist.in index 0004423b15..3b0ef69bcf 100644 --- a/packaging/macos/bundle/shview.plist.in +++ b/packaging/macos/bundle/shview.plist.in @@ -12,7 +12,7 @@ CFBundlePackageType APPL CFBundleShortVersionString ${PROJECT_VERSION} CFBundleVersion ${PROJECT_VERSION} - NSHumanReadableCopyright Copyright (c) 2008-${CURRENT_YEAR} the MRtrix3 contributors + NSHumanReadableCopyright Copyright (c) ${COPYRIGHT_YEAR} the MRtrix3 contributors LSMinimumSystemVersion ${CMAKE_OSX_DEPLOYMENT_TARGET} LSBackgroundOnly 0 NSHighResolutionCapable From 3c804f60f02a89e15a34b682addf6ccf3e231351 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Thu, 6 Jun 2024 20:33:05 +0100 Subject: [PATCH 096/182] Set correct icons for mrview --- cmake/MacOSBundle.cmake | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmake/MacOSBundle.cmake b/cmake/MacOSBundle.cmake index 0bb0fcde20..42bb702a7d 100644 --- a/cmake/MacOSBundle.cmake +++ b/cmake/MacOSBundle.cmake @@ -1,18 +1,17 @@ function(set_bundle_properties executable_name) + set(icon_files ${CMAKE_CURRENT_SOURCE_DIR}/../icons/macos/${executable_name}.icns) if(${executable_name} STREQUAL "mrview") - set(mrtrix_icon_macos ${CMAKE_CURRENT_SOURCE_DIR}/../icons/macos/mrview_doc.icns) - else() - set(mrtrix_icon_macos ${CMAKE_CURRENT_SOURCE_DIR}/../icons/macos/${executable_name}.icns) + list(APPEND icon_files ${CMAKE_CURRENT_SOURCE_DIR}/../icons/macos/mrview_doc.icns) endif() string(TIMESTAMP CURRENT_YEAR "%Y") set(COPYRIGHT_YEAR "2008-${CURRENT_YEAR}" CACHE STRING "Copyright year") - target_sources(${executable_name} PRIVATE ${mrtrix_icon_macos}) + target_sources(${executable_name} PRIVATE ${icon_files}) set_target_properties(${executable_name} PROPERTIES MACOSX_BUNDLE TRUE MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/../packaging/macos/bundle/${executable_name}.plist.in" - RESOURCE ${mrtrix_icon_macos} + RESOURCE "${icon_files}" INSTALL_RPATH "@executable_path/../../../../lib" ) endfunction() From 93f3741c68b1fe8a350b05a155aa7272ff2775e2 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sun, 9 Jun 2024 19:23:48 +1000 Subject: [PATCH 097/182] Major changes to Python handling following cmake adoption - Re-arrange Python source files on the filesystem. API files reside in python/mrtrix3/ (still in a sub-directory called "mrtrix3/" so that IDEs can reasonably identify them, but no need to be in a "lib/" sub-directory in source form). All command source code centralised into python/mrtrix3/commands. - cmake will generate short executable Python files in the build bin/ directory, one for each command. These load the version-matched API and command code from a relative path. This mechanism deprecates file python/bin/mrtrix3.py, and command source files no longer include the "import mrtrix3; mrtrix3.execute()" code at their tail. - Change to the interface of the app._execute() function. Rather than a module, it now accepts as input the usage() and execute() functions. This should allow greater flexibility in how a developer arranges their command code. - Following from point above, there are now three different ways for a developer to arrange their command code on the filesystem: 1. A solitary .py file residing in python/mrtrix3/commands/, which defines both usage() and execute() functions within. 2. A sub-directory residing in python/mrtrix3/commands/, which contains a .py file with the same name as the sub-directory, within which contains the usage() and execute() functions. This is useful for those commands using the "algorithm" concept, as each algorithm can be defined as its own .py file within that sub-directory; but all source code relating to that command is grouped within that directory. 3. A sub-directory residing in python/mrtrix3/commands/, which contains at least two files called usage.py and execute.py, which provide the usage() and execute() function implementations respectively. This will be useful for those scripts where the volume of code is too much for a single source code file. - mrtrix3.algorithm module deprecated. Some functionality is no longer required, and algorithm modules are now loaded directly using importlib instead. List of algorithms available for relevant commands is now explicitly coded in the corresponding __init__.py files. - No longer have to rename source code relating to the 5ttgen command to "_5ttgen"; all handling of module names should now be permissive of leading digits. - cmake can either copy or softlink Python source code files, toggled via envvar "MRTRIX_PYTHON_SOFTLINK". - Deprecate commands blend, convert_bruker, gen_scheme, notfound; commands not utilising the MRtrix3 Python API are not well managed by the new build system, and still do not appear in the online documentation, so will need to be either ported or provided externally. - Files python/mrtrix3/version.py.in and python/mrtrix3/commands/__init__.py.in generate build directory files lib/mrtrix3/version.py and lib/mrtrix3/commands/__init__.py, filled with relevant build-time information. However files python/mrtrix3/version.py and python/mrtrix3/commands/__init__.py are still defined in the source tree, with the relevant variables defined, only that they are void of information. This allows run_pylint to run without even configuring cmake, and should prevent IDEs from producing undefined symbol warnings. --- .github/workflows/checks.yml | 1 - CMakeLists.txt | 1 + cmake/GenPythonCommandsLists.cmake | 40 +++++ cmake/MakePythonExecutable.cmake | 40 +++++ python/CMakeLists.txt | 71 +-------- python/bin/blend | 52 ------- python/bin/convert_bruker | 143 ----------------- python/bin/gen_scheme | 147 ------------------ python/bin/mrtrix3.py | 92 ----------- python/bin/notfound | 38 ----- python/lib/mrtrix3/_5ttgen/__init__.py | 0 python/lib/mrtrix3/_version.py.in | 2 - python/lib/mrtrix3/algorithm.py | 69 -------- python/lib/mrtrix3/dwi2mask/__init__.py | 0 python/lib/mrtrix3/dwi2response/__init__.py | 0 python/lib/mrtrix3/dwibiascorrect/__init__.py | 0 python/lib/mrtrix3/dwinormalise/__init__.py | 0 python/mrtrix3/CMakeLists.txt | 71 +++++++++ python/{lib => }/mrtrix3/__init__.py | 23 +-- python/{lib => }/mrtrix3/app.py | 46 +++--- .../commands/5ttgen/5ttgen.py} | 19 +-- python/mrtrix3/commands/5ttgen/__init__.py | 17 ++ .../commands/5ttgen}/freesurfer.py | 2 +- .../commands/5ttgen}/fsl.py | 0 .../commands/5ttgen}/gif.py | 0 .../commands/5ttgen}/hsvs.py | 6 +- python/mrtrix3/commands/CMakeLists.txt | 110 +++++++++++++ python/mrtrix3/commands/__init__.py | 24 +++ python/mrtrix3/commands/__init__.py.in | 29 ++++ .../commands}/dwi2mask/3dautomask.py | 0 python/mrtrix3/commands/dwi2mask/__init__.py | 27 ++++ .../commands}/dwi2mask/ants.py | 0 .../commands}/dwi2mask/b02template.py | 0 .../commands}/dwi2mask/consensus.py | 5 +- .../commands/dwi2mask/dwi2mask.py} | 24 ++- .../commands}/dwi2mask/fslbet.py | 0 .../commands}/dwi2mask/hdbet.py | 0 .../commands}/dwi2mask/legacy.py | 0 .../commands}/dwi2mask/mean.py | 0 .../commands}/dwi2mask/mtnorm.py | 0 .../commands}/dwi2mask/synthstrip.py | 0 .../commands}/dwi2mask/trace.py | 0 .../mrtrix3/commands/dwi2response/__init__.py | 17 ++ .../commands}/dwi2response/dhollander.py | 0 .../commands/dwi2response/dwi2response.py} | 27 ++-- .../commands}/dwi2response/fa.py | 0 .../commands}/dwi2response/manual.py | 0 .../commands}/dwi2response/msmt_5tt.py | 0 .../commands}/dwi2response/tax.py | 0 .../commands}/dwi2response/tournier.py | 0 .../commands/dwibiascorrect/__init__.py | 17 ++ .../commands}/dwibiascorrect/ants.py | 0 .../dwibiascorrect/dwibiascorrect.py} | 24 +-- .../commands}/dwibiascorrect/fsl.py | 0 .../commands}/dwibiascorrect/mtnorm.py | 0 .../commands/dwibiasnormmask.py} | 10 +- .../dwicat => mrtrix3/commands/dwicat.py} | 13 +- .../commands/dwifslpreproc.py} | 19 +-- .../commands/dwigradcheck.py} | 11 +- .../mrtrix3/commands/dwinormalise/__init__.py | 17 ++ .../commands/dwinormalise/dwinormalise.py} | 22 +-- .../commands}/dwinormalise/group.py | 0 .../commands}/dwinormalise/manual.py | 0 .../commands}/dwinormalise/mtnorm.py | 0 .../commands/dwishellmath.py} | 8 - .../for_each => mrtrix3/commands/for_each.py} | 19 +-- .../commands/labelsgmfirst.py} | 22 +-- .../commands/mask2glass.py} | 8 +- .../commands/mrtrix_cleanup.py} | 8 - .../commands/population_template.py} | 24 +-- .../commands/responsemean.py} | 9 -- python/{lib => }/mrtrix3/fsl.py | 0 python/{lib => }/mrtrix3/image.py | 0 python/{lib => }/mrtrix3/matrix.py | 0 python/{lib => }/mrtrix3/path.py | 23 +-- python/{lib => }/mrtrix3/phaseencoding.py | 0 python/{lib => }/mrtrix3/run.py | 15 +- python/{lib => }/mrtrix3/sh.py | 0 python/{lib => }/mrtrix3/utils.py | 0 python/mrtrix3/version.py | 21 +++ python/mrtrix3/version.py.in | 18 +++ run_pylint | 17 +- .../{_5ttgen => 5ttgen}/FreeSurfer2ACT.txt | 0 .../FreeSurfer2ACT_sgm_amyg_hipp.txt | 0 .../hsvs/AmygSubfields.txt | 0 .../hsvs/HippSubfields.txt | 0 86 files changed, 559 insertions(+), 909 deletions(-) create mode 100644 cmake/GenPythonCommandsLists.cmake create mode 100644 cmake/MakePythonExecutable.cmake delete mode 100755 python/bin/blend delete mode 100755 python/bin/convert_bruker delete mode 100755 python/bin/gen_scheme delete mode 100644 python/bin/mrtrix3.py delete mode 100755 python/bin/notfound delete mode 100644 python/lib/mrtrix3/_5ttgen/__init__.py delete mode 100644 python/lib/mrtrix3/_version.py.in delete mode 100644 python/lib/mrtrix3/algorithm.py delete mode 100644 python/lib/mrtrix3/dwi2mask/__init__.py delete mode 100644 python/lib/mrtrix3/dwi2response/__init__.py delete mode 100644 python/lib/mrtrix3/dwibiascorrect/__init__.py delete mode 100644 python/lib/mrtrix3/dwinormalise/__init__.py create mode 100644 python/mrtrix3/CMakeLists.txt rename python/{lib => }/mrtrix3/__init__.py (77%) rename python/{lib => }/mrtrix3/app.py (97%) rename python/{bin/5ttgen => mrtrix3/commands/5ttgen/5ttgen.py} (87%) mode change 100755 => 100644 create mode 100644 python/mrtrix3/commands/5ttgen/__init__.py rename python/{lib/mrtrix3/_5ttgen => mrtrix3/commands/5ttgen}/freesurfer.py (97%) rename python/{lib/mrtrix3/_5ttgen => mrtrix3/commands/5ttgen}/fsl.py (100%) rename python/{lib/mrtrix3/_5ttgen => mrtrix3/commands/5ttgen}/gif.py (100%) rename python/{lib/mrtrix3/_5ttgen => mrtrix3/commands/5ttgen}/hsvs.py (99%) create mode 100644 python/mrtrix3/commands/CMakeLists.txt create mode 100644 python/mrtrix3/commands/__init__.py create mode 100644 python/mrtrix3/commands/__init__.py.in rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/3dautomask.py (100%) create mode 100644 python/mrtrix3/commands/dwi2mask/__init__.py rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/ants.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/b02template.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/consensus.py (98%) rename python/{bin/dwi2mask => mrtrix3/commands/dwi2mask/dwi2mask.py} (86%) mode change 100755 => 100644 rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/fslbet.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/hdbet.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/legacy.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/mean.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/mtnorm.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/synthstrip.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2mask/trace.py (100%) create mode 100644 python/mrtrix3/commands/dwi2response/__init__.py rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2response/dhollander.py (100%) rename python/{bin/dwi2response => mrtrix3/commands/dwi2response/dwi2response.py} (89%) mode change 100755 => 100644 rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2response/fa.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2response/manual.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2response/msmt_5tt.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2response/tax.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwi2response/tournier.py (100%) create mode 100644 python/mrtrix3/commands/dwibiascorrect/__init__.py rename python/{lib/mrtrix3 => mrtrix3/commands}/dwibiascorrect/ants.py (100%) rename python/{bin/dwibiascorrect => mrtrix3/commands/dwibiascorrect/dwibiascorrect.py} (84%) mode change 100755 => 100644 rename python/{lib/mrtrix3 => mrtrix3/commands}/dwibiascorrect/fsl.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwibiascorrect/mtnorm.py (100%) rename python/{bin/dwibiasnormmask => mrtrix3/commands/dwibiasnormmask.py} (99%) mode change 100755 => 100644 rename python/{bin/dwicat => mrtrix3/commands/dwicat.py} (98%) mode change 100755 => 100644 rename python/{bin/dwifslpreproc => mrtrix3/commands/dwifslpreproc.py} (99%) mode change 100755 => 100644 rename python/{bin/dwigradcheck => mrtrix3/commands/dwigradcheck.py} (96%) mode change 100755 => 100644 create mode 100644 python/mrtrix3/commands/dwinormalise/__init__.py rename python/{bin/dwinormalise => mrtrix3/commands/dwinormalise/dwinormalise.py} (79%) mode change 100755 => 100644 rename python/{lib/mrtrix3 => mrtrix3/commands}/dwinormalise/group.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwinormalise/manual.py (100%) rename python/{lib/mrtrix3 => mrtrix3/commands}/dwinormalise/mtnorm.py (100%) rename python/{bin/dwishellmath => mrtrix3/commands/dwishellmath.py} (96%) mode change 100755 => 100644 rename python/{bin/for_each => mrtrix3/commands/for_each.py} (97%) mode change 100755 => 100644 rename python/{bin/labelsgmfirst => mrtrix3/commands/labelsgmfirst.py} (92%) mode change 100755 => 100644 rename python/{bin/mask2glass => mrtrix3/commands/mask2glass.py} (96%) mode change 100755 => 100644 rename python/{bin/mrtrix_cleanup => mrtrix3/commands/mrtrix_cleanup.py} (98%) mode change 100755 => 100644 rename python/{bin/population_template => mrtrix3/commands/population_template.py} (98%) mode change 100755 => 100644 rename python/{bin/responsemean => mrtrix3/commands/responsemean.py} (97%) mode change 100755 => 100644 rename python/{lib => }/mrtrix3/fsl.py (100%) rename python/{lib => }/mrtrix3/image.py (100%) rename python/{lib => }/mrtrix3/matrix.py (100%) rename python/{lib => }/mrtrix3/path.py (87%) rename python/{lib => }/mrtrix3/phaseencoding.py (100%) rename python/{lib => }/mrtrix3/run.py (98%) rename python/{lib => }/mrtrix3/sh.py (100%) rename python/{lib => }/mrtrix3/utils.py (100%) create mode 100644 python/mrtrix3/version.py create mode 100644 python/mrtrix3/version.py.in rename share/mrtrix3/{_5ttgen => 5ttgen}/FreeSurfer2ACT.txt (100%) rename share/mrtrix3/{_5ttgen => 5ttgen}/FreeSurfer2ACT_sgm_amyg_hipp.txt (100%) rename share/mrtrix3/{_5ttgen => 5ttgen}/hsvs/AmygSubfields.txt (100%) rename share/mrtrix3/{_5ttgen => 5ttgen}/hsvs/HippSubfields.txt (100%) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4a4487d3ce..17d76f1d89 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -268,7 +268,6 @@ jobs: - name: pylint run: | - echo "__version__ = 'pylint testing' #pylint: disable=unused-variable" > ./python/lib/mrtrix3/_version.py ./run_pylint || { cat pylint.log; false; } - name: check copyright headers diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b0f6c2b63..9d1f56390d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ option(MRTRIX_STL_DEBUGGING "Enable STL debug mode" OFF) option(MRTRIX_BUILD_TESTS "Build tests executables" OFF) option(MRTRIX_STRIP_CONDA "Strip ananconda/mininconda from PATH to avoid conflicts" ON) option(MRTRIX_USE_PCH "Use precompiled headers" ON) +option(MRTRIX_PYTHON_SOFTLINK "Build directory softlink to Python source code rather than copying" ON) if(MRTRIX_BUILD_TESTS) if(CMAKE_VERSION VERSION_GREATER 3.17) diff --git a/cmake/GenPythonCommandsLists.cmake b/cmake/GenPythonCommandsLists.cmake new file mode 100644 index 0000000000..ebcef2728d --- /dev/null +++ b/cmake/GenPythonCommandsLists.cmake @@ -0,0 +1,40 @@ +file( + GLOB CPP_COMMAND_FILES + ${CMAKE_SOURCE_DIR}/cmd/*.cpp +) + +file( + GLOB PYTHON_ROOT_ENTRIES + ${CMAKE_SOURCE_DIR}/python/mrtrix3/commands/* +) + +set(MRTRIX_CPP_COMMAND_LIST "") +foreach(CPP_COMMAND_FILE ${CPP_COMMAND_FILES}) + get_filename_component(CPP_COMMAND_NAME ${CPP_COMMAND_FILE} NAME_WE) + if(MRTRIX_CPP_COMMAND_LIST STREQUAL "") + set(MRTRIX_CPP_COMMAND_LIST "\"${CPP_COMMAND_NAME}\"") + else() + set(MRTRIX_CPP_COMMAND_LIST "${MRTRIX_CPP_COMMAND_LIST},\n \"${CPP_COMMAND_NAME}\"") + endif() +endforeach() + +set(MRTRIX_PYTHON_COMMAND_LIST "") +foreach(PYTHON_ROOT_ENTRY ${PYTHON_ROOT_ENTRIES}) + get_filename_component(PYTHON_COMMAND_NAME ${PYTHON_ROOT_ENTRY} NAME_WE) + if(NOT ${PYTHON_COMMAND_NAME} STREQUAL "CMakeLists" AND NOT ${PYTHON_COMMAND_NAME} STREQUAL "__init__") + if(MRTRIX_PYTHON_COMMAND_LIST STREQUAL "") + set(MRTRIX_PYTHON_COMMAND_LIST "\"${PYTHON_COMMAND_NAME}\"") + else() + set(MRTRIX_PYTHON_COMMAND_LIST "${MRTRIX_PYTHON_COMMAND_LIST},\n \"${PYTHON_COMMAND_NAME}\"") + endif() + endif() +endforeach() +message(VERBOSE "Completed GenPythonCommandsList() function") +message(VERBOSE "Formatted list of MRtrix3 C++ commands: ${MRTRIX_CPP_COMMAND_LIST}") +message(VERBOSE "Formatted list of MRtrix3 Python commands: ${MRTRIX_PYTHON_COMMAND_LIST}") + +configure_file( + ${SRC} + ${DST} + @ONLY +) diff --git a/cmake/MakePythonExecutable.cmake b/cmake/MakePythonExecutable.cmake new file mode 100644 index 0000000000..38b55bcd84 --- /dev/null +++ b/cmake/MakePythonExecutable.cmake @@ -0,0 +1,40 @@ +# Creates within the bin/ sub-directory of the project build directory +# a short Python executable that is used to run a Python command from the terminal +# Receives name of the command as ${CMDNAME}; output build directory as ${BUILDDIR} +set(BINPATH "${BUILDDIR}/temporary/python/${CMDNAME}") +file(WRITE ${BINPATH} "#!/usr/bin/python3\n") +file(APPEND ${BINPATH} "# -*- coding: utf-8 -*-\n") +file(APPEND ${BINPATH} "\n") +file(APPEND ${BINPATH} "import importlib\n") +file(APPEND ${BINPATH} "import os\n") +file(APPEND ${BINPATH} "import sys\n") +file(APPEND ${BINPATH} "\n") +file(APPEND ${BINPATH} "mrtrix_lib_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib'))\n") +file(APPEND ${BINPATH} "sys.path.insert(0, mrtrix_lib_path)\n") +file(APPEND ${BINPATH} "from mrtrix3.app import _execute\n") +# Three possible interfaces: +# 1. Standalone file residing in commands/ +# 2. File stored in location commands//.py, which will contain usage() and execute() functions +# 3. Two files stored at commands//usage.py and commands//execute.py, defining the two corresponding functions +# TODO Port population_template to type 3; both for readability and to ensure that it works +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/__init__.py") + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/usage.py" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/execute.py") + file(APPEND ${BINPATH} "module_usage = importlib.import_module('.usage', 'mrtrix3.commands.${CMDNAME}')\n") + file(APPEND ${BINPATH} "module_execute = importlib.import_module('.execute', 'mrtrix3.commands.${CMDNAME}')\n") + file(APPEND ${BINPATH} "_execute(module_usage.usage, module_execute.execute)\n") + elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/${CMDNAME}.py") + file(APPEND ${BINPATH} "module = importlib.import_module('.${CMDNAME}', 'mrtrix3.commands.${CMDNAME}')\n") + file(APPEND ${BINPATH} "_execute(module.usage, module.execute)\n") + else() + message(FATAL_ERROR "Malformed filesystem structure for Python command ${CMDNAME}") + endif() +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}.py") + file(APPEND ${BINPATH} "module = importlib.import_module('.${CMDNAME}', 'mrtrix3.commands')\n") + file(APPEND ${BINPATH} "_execute(module.usage, module.execute)\n") +else() + message(FATAL_ERROR "Malformed filesystem structure for Python command ${CMDNAME}") +endif() +file(COPY ${BINPATH} DESTINATION ${BUILDDIR}/bin + FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ +) + diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 46187b89c6..bf439217e3 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -1,71 +1,2 @@ -set(PYTHON_VERSION_FILE ${CMAKE_CURRENT_SOURCE_DIR}/lib/mrtrix3/_version.py) +add_subdirectory(mrtrix3) -find_package(Git QUIET) - -file(GLOB_RECURSE PYTHON_BIN_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/bin/* -) - -file(GLOB_RECURSE PYTHON_LIB_FILES - ${CMAKE_CURRENT_SOURCE_DIR}/lib/* -) - -add_custom_target(Python SOURCES - ${PYTHON_BIN_FILES} -) - -# We generate the version file at configure time, -# so tools like Pylint can run without building the project -execute_process( - COMMAND ${CMAKE_COMMAND} - -D GIT_EXECUTABLE=${GIT_EXECUTABLE} - -D MRTRIX_BASE_VERSION=${MRTRIX_BASE_VERSION} - -D DST=${PYTHON_VERSION_FILE} - -D SRC=${CMAKE_CURRENT_SOURCE_DIR}/lib/mrtrix3/_version.py.in - -P ${PROJECT_SOURCE_DIR}/cmake/FindVersion.cmake - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} -) - -add_custom_target(CopyPythonFiles ALL) -set(PYTHON_BUILD_BIN_FILES "") - -foreach(BIN_FILE ${PYTHON_BIN_FILES}) - get_filename_component(BIN_FILE_NAME ${BIN_FILE} NAME) - set(DST_BIN_FILE ${PROJECT_BINARY_DIR}/bin/${BIN_FILE_NAME}) - add_custom_command( - TARGET CopyPythonFiles - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BIN_FILE} ${DST_BIN_FILE} - DEPENDS ${BIN_FILE} - ) - list(APPEND PYTHON_BUILD_BIN_FILES ${DST_BIN_FILE}) -endforeach() - -add_custom_command( - TARGET CopyPythonFiles - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CMAKE_CURRENT_SOURCE_DIR}/lib" "${PROJECT_BINARY_DIR}/lib" -) - -set_target_properties(CopyPythonFiles - PROPERTIES ADDITIONAL_CLEAN_FILES - "${PYTHON_BUILD_BIN_FILES};${PROJECT_BINARY_DIR}/lib" -) - -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bin/ - DESTINATION ${CMAKE_INSTALL_BINDIR} - USE_SOURCE_PERMISSIONS - PATTERN "__pycache__" EXCLUDE - PATTERN ".pyc" EXCLUDE -) - -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib/ - DESTINATION ${CMAKE_INSTALL_LIBDIR} - USE_SOURCE_PERMISSIONS - PATTERN "__pycache__" EXCLUDE - PATTERN "*.py.in" EXCLUDE - PATTERN ".pyc" EXCLUDE -) - -install(FILES ${PYTHON_VERSION_FILE} - DESTINATION ${CMAKE_INSTALL_LIBDIR}/mrtrix3 -) diff --git a/python/bin/blend b/python/bin/blend deleted file mode 100755 index 8d47ce1e6b..0000000000 --- a/python/bin/blend +++ /dev/null @@ -1,52 +0,0 @@ -#!/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/. - -import os -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') - sys.stderr.write('eg: blend folder1 folder2 20 output_folder\n') - sys.exit(1) - -INPUT_FOLDER_1 = sys.argv[1] -INPUT_FOLDER_2 = sys.argv[2] -FILE_LIST_1 = sorted(os.listdir(INPUT_FOLDER_1)) -FILE_LIST_2 = sorted(os.listdir(INPUT_FOLDER_2)) -NUM_OVERLAP = int(sys.argv[3]) -OUTPUT_FOLDER = sys.argv[4] - -if not os.path.exists(OUTPUT_FOLDER): - os.mkdir(OUTPUT_FOLDER) - -NUM_OUTPUT_FRAMES = len(FILE_LIST_1) + len(FILE_LIST_2) - NUM_OVERLAP -for i in range(NUM_OUTPUT_FRAMES): - file_name = f'frame{i:%05d}.png' - if i <= len(FILE_LIST_1) - NUM_OVERLAP: - 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(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(f'cp -L {INPUT_FOLDER_2}/{FILE_LIST_2[i2]} {OUTPUT_FOLDER}/{file_name}') diff --git a/python/bin/convert_bruker b/python/bin/convert_bruker deleted file mode 100755 index dff88c9498..0000000000 --- a/python/bin/convert_bruker +++ /dev/null @@ -1,143 +0,0 @@ -#!/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/. - -import sys, os.path - -if len (sys.argv) != 3: - sys.stderr.write("usage: convert_bruker 2dseq header.mih\n") - sys.exit (0) - - -#if os.path.basename (sys.argv[1]) != '2dseq': - #print ("expected '2dseq' file as first argument") - #sys.exit (1) - -if not sys.argv[2].endswith ('.mih'): - sys.stderr.write("expected .mih suffix as the second argument\n") - sys.exit (1) - - - -def main(): - - with open (os.path.join (os.path.dirname (sys.argv[1]), 'reco'), encoding='utf-8') as file_reco: - lines = file_reco.read().split ('##$') - - with open (os.path.join (os.path.dirname (sys.argv[1]), '../../acqp'), encoding='utf-8') as file_acqp: - lines += file_acqp.read().split ('##$') - - with open (os.path.join (os.path.dirname (sys.argv[1]), '../../method'), encoding='utf-8') as file_method: - lines += file_method.read().split ('##$') - - - for line in lines: - line = line.lower() - if line.startswith ('reco_size='): - mat_size = line.splitlines()[1].split() - print ('mat_size', mat_size) - elif line.startswith ('nslices='): - nslices = line.split('=')[1].split()[0] - print ('nslices', nslices) - elif line.startswith ('acq_time_points='): - nacq = len (line.split('\n',1)[1].split()) - print ('nacq', nacq) - elif line.startswith ('reco_wordtype='): - wtype = line.split('=')[1].split()[0] - print ('wtype', wtype) - elif line.startswith ('reco_byte_order='): - byteorder = line.split('=')[1].split()[0] - print ('byteorder', byteorder) - elif line.startswith ('pvm_spatresol='): - res = line.splitlines()[1].split() - print ('res', res) - elif line.startswith ('pvm_spackarrslicedistance='): - slicethick = line.splitlines()[1].split()[0] - print ('slicethick', slicethick) - elif line.startswith ('pvm_dweffbval='): - bval = line.split('\n',1)[1].split() - print ('bval', bval) - elif line.startswith ('pvm_dwgradvec='): - bvec = line.split('\n',1)[1].split() - print ('bvec', bvec) - - - with open (sys.argv[2], 'w', encoding='utf-8') as file_out: - file_out.write ('mrtrix image\ndim: ' + mat_size[0] + ',' + mat_size[1]) - if len(mat_size) > 2: - file_out.write (',' + str(mat_size[2])) - else: - try: - nslices #pylint: disable=pointless-statement - file_out.write (',' + str(nslices)) - except NameError: - pass - - try: - nacq #pylint: disable=pointless-statement - file_out.write (',' + str(nacq)) - except NameError: - pass - - file_out.write ('\nvox: ' + str(res[0]) + ',' + str(res[1])) - if len(res) > 2: - file_out.write (',' + str(res[2])) - else: - try: - slicethick #pylint: disable=pointless-statement - file_out.write (',' + str(slicethick)) - except NameError: - pass - try: - nacq #pylint: disable=pointless-statement - file_out.write (',') - except NameError: - pass - - file_out.write ('\ndatatype: ') - if wtype == '_16bit_sgn_int': - file_out.write ('int16') - elif wtype == '_32bit_sgn_int': - file_out.write ('int32') - - if byteorder=='littleendian': - file_out.write ('le') - else: - file_out.write ('be') - - file_out.write ('\nlayout: +0,+1') - try: - nslices #pylint: disable=pointless-statement - file_out.write (',+2') - except NameError: - pass - try: - nacq #pylint: disable=pointless-statement - file_out.write (',+3') - except NameError: - pass - - file_out.write ('\nfile: ' + sys.argv[1] + '\n') - - try: - assert len(bvec) == 3*len(bval) - bvec = [ bvec[n:n+3] for n in range(0,len(bvec),3) ] - for direction, value in zip(bvec, bval): - file_out.write ('dw_scheme: ' + direction[0] + ',' + direction[1] + ',' + str(-float(direction[2])) + ',' + value + '\n') - except AssertionError: - pass - -main() diff --git a/python/bin/gen_scheme b/python/bin/gen_scheme deleted file mode 100755 index 406a7c965b..0000000000 --- a/python/bin/gen_scheme +++ /dev/null @@ -1,147 +0,0 @@ -#!/bin/bash - -# 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/. - -set -e - -if [ "$#" -eq 0 ]; then - echo " - gen_scheme: part of the MRtrix package - -SYNOPSIS - - gen_scheme numPE [ bvalue ndir ]... - - numPE the number of phase-encoding directions to be included in - the scheme (most scanners will only support a single PE - direction per sequence, so this will typically be 1). - - bvalue the b-value of the shell - - ndir the number of directions to include in the shell - - -DESCRIPTION - - This script generates a diffusion gradient table according to the - parameters specified. For most users, something like the following would be - appropriate: - - gen_scheme 1 0 5 750 20 3000 60 - - which will geneate a multi-shell diffusion gradient table with a single - phase-encode direction, consisting of 5 b=0, 20 b=750, and 60 b=3000 - volumes. - - The gradient table is generated using the following procedure: - - - The directions for each shell are optimally distributed using a bipolar - electrostatic repulsion model (using the command 'dirgen'). - - - These are then split into numPE sets (if numPE != 1) using a brute-force - random search for the most optimally-distributed subsets (using the command - 'dirsplit'). - - - Each of the resulting sets is then rearranged by inversion of individual - directions through the origin (i.e. direction vector x => -x) using a - brute-force random search to find the most optimal combination in terms - of unipolar repulsion: this ensures near-uniform distribution over the - sphere to avoid biases in terms of eddy-current distortions, as - recommended for FSL's EDDY command (this step uses the 'dirflip' command). - - - Finally, all the individual subsets are merged (using the 'dirmerge' - command) into a single gradient table, in such a way as to maintain - near-uniformity upon truncation (in as much as is possible), in both - b-value and directional domains. In other words, the approach aims to - ensure that if the acquisition is cut short, the set of volumes acquired - nonetheless contains the same relative proportions of b-values as - specified, with directions that are near-uniformly distributed. - - The primary output of this command is a file called 'dw_scheme.txt', - consisting of a 5-column table, with one line per volume. Each column - consists of [ x y z b PE ], where [ x y z ] is the unit direction vector, b - is the b-value in unit of s/mm², and PE is a integer ID from 1 to numPE. - - The command also retains all of the subsets generated along the way, which - you can safely delete once the command has completed. Since this can - consist of quite a few files, it is recommended to run this command within - its own temporary folder. - - See also the 'dirstat' command to obtain simple metrics of quality for the - set produced. -" - exit 1 -else - - nPE=$1 - if [ $nPE -ne 1 ] && [ $nPE -ne 2 ] && [ $nPE -ne 4 ]; then - echo "ERROR: numPE should be one of 1, 2, 4" - exit 1 - fi - - shift - # store args for re-use: - ARGS=( "$@" ) - - # print parsed info for sanity-checking: - echo "generating scheme with $nPE phase-encode directions, with:" - while [ ! -z "$1" ]; do - echo " b = $1: $2 directions" - shift 2 - done - - perm="" #"-perm 1000" - - # reset args: - set -- "${ARGS[@]}" - merge="" - - while [ ! -z "$1" ]; do - echo "=====================================" - echo "generating directions for b = $1..." - echo "=====================================" - - merge=$merge" "$1 - - dirgen $2 dirs-b$1-$2.txt -force - if [ $nPE -gt 1 ]; then - dirsplit dirs-b$1-$2.txt dirs-b$1-$2-{1..2}.txt -force $perm - if [ $nPE -gt 2 ]; then - dirsplit dirs-b$1-$2-1.txt dirs-b$1-$2-1{1..2}.txt -force $perm - dirsplit dirs-b$1-$2-2.txt dirs-b$1-$2-2{1..2}.txt -force $perm - # TODO: the rest... - for n in dirs-b$1-$2-{1,2}{1,2}.txt; do - dirflip $n ${n%.txt}-flip.txt -force $perm - merge=$merge" "${n%.txt}-flip.txt - done - else - for n in dirs-b$1-$2-{1,2}.txt; do - dirflip $n ${n%.txt}-flip.txt -force $perm - merge=$merge" "${n%.txt}-flip.txt - done - fi - else - dirflip dirs-b$1-$2.txt dirs-b$1-$2-flip.txt -force $perm - merge=$merge" "dirs-b$1-$2-flip.txt - fi - - shift 2 - done - - echo $merge - dirmerge $nPE $merge dw_scheme.txt -force -fi - diff --git a/python/bin/mrtrix3.py b/python/bin/mrtrix3.py deleted file mode 100644 index 12c0164325..0000000000 --- a/python/bin/mrtrix3.py +++ /dev/null @@ -1,92 +0,0 @@ - -# Copyright (c) 2008-2019 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/. - -import os, sys - -try: - # since importlib code below only works on Python 3.5+ - # https://stackoverflow.com/a/50395128 - if sys.version_info < (3,5): - raise ImportError - - import importlib.util - - def imported(lib_path): - try: - spec = importlib.util.spec_from_file_location('mrtrix3', os.path.join (lib_path, 'mrtrix3', '__init__.py')) - module = importlib.util.module_from_spec (spec) - sys.modules[spec.name] = module - spec.loader.exec_module (module) - return True - except ImportError: - return False - -except ImportError: - try: - import imp - except ImportError: - print ('failed to import either imp or importlib module!') - sys.exit(1) - - def imported(lib_path): - success = False - fp = None - try: - fp, pathname, description = imp.find_module('mrtrix3', [ lib_path ]) - imp.load_module('mrtrix3', fp, pathname, description) - success = True - except ImportError: - pass - finally: - if fp: - fp.close() - return success - - -# Can the MRtrix3 Python modules be found based on their relative location to this file? -# Note that this includes the case where this file is a softlink within an external module, -# which provides a direct link to the core installation -if not imported (os.path.normpath (os.path.join ( \ - os.path.dirname (os.path.realpath (__file__)), os.pardir, 'lib') )): - - # If this file is a duplicate, which has been stored in an external module, - # we may be able to figure out the location of the core library using the - # build script. - - # case 1: build is a symbolic link: - if not imported (os.path.join (os.path.dirname (os.path.realpath ( \ - os.path.join (os.path.dirname(__file__), os.pardir, 'build'))), 'lib')): - - # case 2: build is a file containing the path to the core build script: - try: - with open (os.path.join (os.path.dirname(__file__), os.pardir, 'build')) as fp: - for line in fp: - build_path = line.split ('#',1)[0].strip() - if build_path: - break - except IOError: - pass - - if not imported (os.path.join (os.path.dirname (build_path), 'lib')): - - sys.stderr.write(''' -ERROR: Unable to locate MRtrix3 Python modules - -For detailed instructions, please refer to: -https://mrtrix.readthedocs.io/en/latest/tips_and_tricks/external_modules.html -''') - sys.stderr.flush() - sys.exit(1) diff --git a/python/bin/notfound b/python/bin/notfound deleted file mode 100755 index b9c5be81b4..0000000000 --- a/python/bin/notfound +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -# 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/. - -if [ $# -eq 0 ]; then - cat << 'HELP_PAGE' -USAGE: - $ notfound base_directory search_string - - This is a simple script designed to help identify subjects that do not yet have a specific file generated. For example when adding new patients to a study. It is designed to be used when each patient has a folder containing their images. - - For example: - $ notfound study_folder fod.mif - will identify all subject folders (e.g. study_folder/subject001, study_folder/subject002, ...) that do NOT contain a file fod.mif - - Note that this can be used in combination with the foreach script. For example: - $ foreach $(notfound study_folder fod.mif) : dwi2fod IN/dwi.mif IN/response.txt IN/fod.mif -HELP_PAGE - -exit 1 - -fi - -find ${1} -mindepth 1 -maxdepth 1 \( -type l -o -type d \) '!' -exec test -e "{}/${2}" ';' -print - diff --git a/python/lib/mrtrix3/_5ttgen/__init__.py b/python/lib/mrtrix3/_5ttgen/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/lib/mrtrix3/_version.py.in b/python/lib/mrtrix3/_version.py.in deleted file mode 100644 index b7182e8f6f..0000000000 --- a/python/lib/mrtrix3/_version.py.in +++ /dev/null @@ -1,2 +0,0 @@ -__version__ = "@MRTRIX_VERSION@" #pylint: disable=unused-variable -__tag__ = "@MRTRIX_GIT_TAG@" #pylint: disable=unused-variable \ No newline at end of file diff --git a/python/lib/mrtrix3/algorithm.py b/python/lib/mrtrix3/algorithm.py deleted file mode 100644 index dcc8bc5bba..0000000000 --- a/python/lib/mrtrix3/algorithm.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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/. - -# Set of functionalities for when a single script has many 'algorithms' that may be invoked, -# i.e. the script deals with generating a particular output, but there are a number of -# processes to select from, each of which is capable of generating that output. - - -import importlib, inspect, os, pkgutil, sys - - - -# Helper function for finding where the files representing different script algorithms will be stored -# These will be in a sub-directory relative to this library file -def _algorithms_path(): - from mrtrix3 import path #pylint: disable=import-outside-toplevel - return os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(inspect.getouterframes(inspect.currentframe())[-1][1])), os.pardir, 'lib', 'mrtrix3', path.script_subdir_name())) - - - -# This function needs to be safe to run in order to populate the help page; that is, no app initialisation has been run -def get_list(): #pylint: disable=unused-variable - from mrtrix3 import app #pylint: disable=import-outside-toplevel - algorithm_list = [ ] - for filename in os.listdir(_algorithms_path()): - filename = filename.split('.') - if len(filename) == 2 and filename[1] == 'py' and filename[0] != '__init__': - algorithm_list.append(filename[0]) - algorithm_list = sorted(algorithm_list) - app.debug('Found algorithms: ' + str(algorithm_list)) - return algorithm_list - - - -# Note: This function essentially duplicates the current state of app.cmdline in order for command-line -# options common to all algorithms of a particular script to be applicable once any particular sub-parser -# is invoked. Therefore this function must be called _after_ all such options are set up. -def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app, path #pylint: disable=import-outside-toplevel - sys.path.insert(0, os.path.realpath(os.path.join(_algorithms_path(), os.pardir))) - initlist = [ ] - # Don't let Python 3 try to read incompatible .pyc files generated by Python 2 for no-longer-existent .py files - pylist = get_list() - base_parser = app.Parser(description='Base parser for construction of subparsers', parents=[cmdline]) - subparsers = cmdline.add_subparsers(title='Algorithm choices', help='Select the algorithm to be used to complete the script operation; additional details and options become available once an algorithm is nominated. Options are: ' + ', '.join(get_list()), dest='algorithm') - for dummy_importer, package_name, dummy_ispkg in pkgutil.iter_modules( [ _algorithms_path() ] ): - if package_name in pylist: - module = importlib.import_module(path.script_subdir_name() + '.' + package_name) - module.usage(base_parser, subparsers) - initlist.extend(package_name) - app.debug('Initialised algorithms: ' + str(initlist)) - - - -def get_module(name): #pylint: disable=unused-variable - from mrtrix3 import path #pylint: disable=import-outside-toplevel - return sys.modules[path.script_subdir_name() + '.' + name] diff --git a/python/lib/mrtrix3/dwi2mask/__init__.py b/python/lib/mrtrix3/dwi2mask/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/lib/mrtrix3/dwi2response/__init__.py b/python/lib/mrtrix3/dwi2response/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/lib/mrtrix3/dwibiascorrect/__init__.py b/python/lib/mrtrix3/dwibiascorrect/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/lib/mrtrix3/dwinormalise/__init__.py b/python/lib/mrtrix3/dwinormalise/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/mrtrix3/CMakeLists.txt b/python/mrtrix3/CMakeLists.txt new file mode 100644 index 0000000000..c6f0613852 --- /dev/null +++ b/python/mrtrix3/CMakeLists.txt @@ -0,0 +1,71 @@ +add_subdirectory(commands) + +set(PYTHON_VERSION_FILE ${PROJECT_BINARY_DIR}/lib/mrtrix3/version.py) + +find_package(Git QUIET) + +file(GLOB PYTHON_LIB_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/*.py +) + +if(MRTRIX_PYTHON_SOFTLINK) + set(PYTHON_API_TARGET_NAME "LinkPythonAPIFiles") + set(PYTHON_API_FUNCTION_NAME "create_symlink") +else() + set(PYTHON_API_TARGET_NAME "CopyPythonAPIFiles") + set(PYTHON_API_FUNCTION_NAME "copy_if_different") +endif() + +add_custom_target(${PYTHON_API_TARGET_NAME} ALL) +add_custom_command( + TARGET ${PYTHON_API_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/lib/mrtrix3 +) + +set(PYTHON_BUILD_FILES "") +foreach(PYTHON_LIB_FILE ${PYTHON_LIB_FILES}) + get_filename_component(LIB_FILE_NAME ${PYTHON_LIB_FILE} NAME) + # File "version.py" gets generated in the build directory by cmake based on "version.py.in"; + # this file is only present for pylint, and should be skipped during build + if (${LIB_FILE_NAME} STREQUAL "version.py") + continue() + endif() + set(DST_LIB_FILE ${PROJECT_BINARY_DIR}/lib/mrtrix3/${LIB_FILE_NAME}) + add_custom_command( + TARGET ${PYTHON_API_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} -E ${PYTHON_API_FUNCTION_NAME} ${PYTHON_LIB_FILE} ${DST_LIB_FILE} + DEPENDS ${LIB_FILE} + ) + list(APPEND PYTHON_BUILD_FILES ${DST_LIB_FILE}) +endforeach() + +set_target_properties(${PYTHON_API_TARGET_NAME} + PROPERTIES ADDITIONAL_CLEAN_FILES + "${PYTHON_BUILD_FILES};${PROJECT_BINARY_DIR}/lib" +) + +add_custom_target(MakePythonVersionFile ALL) +add_dependencies(MakePythonVersionFile ${PYTHON_API_TARGET_NAME}) +add_custom_command( + TARGET MakePythonVersionFile + COMMAND ${CMAKE_COMMAND} + -D GIT_EXECUTABLE=${GIT_EXECUTABLE} + -D MRTRIX_BASE_VERSION=${MRTRIX_BASE_VERSION} + -D DST=${PYTHON_VERSION_FILE} + -D SRC=${CMAKE_CURRENT_SOURCE_DIR}/version.py.in + -P ${PROJECT_SOURCE_DIR}/cmake/FindVersion.cmake + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) +set_target_properties(MakePythonVersionFile + PROPERTIES ADDITIONAL_CLEAN_FILES ${PYTHON_VERSION_FILE} +) + +install(FILES ${PYTHON_LIB_FILES} + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/mrtrix3 +) + +install(FILES ${PYTHON_VERSION_FILE} + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/mrtrix3 +) diff --git a/python/lib/mrtrix3/__init__.py b/python/mrtrix3/__init__.py similarity index 77% rename from python/lib/mrtrix3/__init__.py rename to python/mrtrix3/__init__.py index aff056c52a..fd2cf70a98 100644 --- a/python/lib/mrtrix3/__init__.py +++ b/python/mrtrix3/__init__.py @@ -13,9 +13,9 @@ # # For more details, see http://www.mrtrix.org/. -import inspect, os, shlex, sys +import os, shlex, sys from collections import namedtuple -from mrtrix3._version import __version__ +from .version import VERSION @@ -33,14 +33,9 @@ class MRtrixError(MRtrixBaseError): #pylint: disable=unused-variable COMMAND_HISTORY_STRING = sys.argv[0] for arg in sys.argv[1:]: COMMAND_HISTORY_STRING += ' ' + shlex.quote(arg) # Use quotation marks only if required - COMMAND_HISTORY_STRING += ' (version=' + __version__ + ')' + COMMAND_HISTORY_STRING += ' (version=' + VERSION + ')' -# Location of binaries that belong to the same MRtrix3 installation as the Python library being invoked -BIN_PATH = os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(os.path.abspath(__file__))), os.pardir, os.pardir, 'bin')) -# Must remove the '.exe' from Windows binary executables -EXE_LIST = [ os.path.splitext(name)[0] for name in os.listdir(BIN_PATH) ] #pylint: disable=unused-variable - # 'CONFIG' is a dictionary containing those entries present in the MRtrix config files # Can add default values here that would otherwise appear in multiple locations @@ -49,11 +44,13 @@ class MRtrixError(MRtrixBaseError): #pylint: disable=unused-variable } + # Codes for printing information to the terminal ANSICodes = namedtuple('ANSI', 'lineclear clear console debug error execute warn') ANSI = ANSICodes('\033[0K', '', '', '', '', '', '') #pylint: disable=unused-variable + # Load the MRtrix configuration files here, and create a dictionary # Load system config first, user second: Allows user settings to override for config_path in [ os.environ.get ('MRTRIX_CONFIGFILE', os.path.join(os.path.sep, 'etc', 'mrtrix.conf')), @@ -72,19 +69,9 @@ class MRtrixError(MRtrixBaseError): #pylint: disable=unused-variable - - - # Set up terminal special characters now, since they may be dependent on the config file def setup_ansi(): global ANSI if sys.stderr.isatty() and not ('TerminalColor' in CONFIG and CONFIG['TerminalColor'].lower() in ['no', 'false', '0']): ANSI = ANSICodes('\033[0K', '\033[0m', '\033[03;32m', '\033[03;34m', '\033[01;31m', '\033[03;36m', '\033[00;31m') #pylint: disable=unused-variable setup_ansi() - - - -# Execute a command -def execute(): #pylint: disable=unused-variable - from . import app #pylint: disable=import-outside-toplevel - app._execute(inspect.getmodule(inspect.stack()[1][0])) # pylint: disable=protected-access diff --git a/python/lib/mrtrix3/app.py b/python/mrtrix3/app.py similarity index 97% rename from python/lib/mrtrix3/app.py rename to python/mrtrix3/app.py index 27b6189cbe..a3c205aeb1 100644 --- a/python/lib/mrtrix3/app.py +++ b/python/mrtrix3/app.py @@ -13,10 +13,10 @@ # # For more details, see http://www.mrtrix.org/. -import argparse, inspect, math, os, pathlib, random, shlex, shutil, signal, string, subprocess, sys, textwrap, time +import argparse, importlib, 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__ +from mrtrix3 import utils, version + # These global constants can / should be accessed directly by scripts: @@ -110,14 +110,13 @@ -# Generally preferable to use: -# "import mrtrix3" -# "mrtrix3.execute()" -# , rather than executing this function directly -def _execute(module): #pylint: disable=unused-variable +# This function gets executed by the corresponding cmake-generated Python executable +def _execute(usage_function, execute_function): #pylint: disable=unused-variable from mrtrix3 import run #pylint: disable=import-outside-toplevel global ARGS, CMDLINE, CONTINUE_OPTION, DO_CLEANUP, FORCE_OVERWRITE, NUM_THREADS, SCRATCH_DIR, VERBOSITY + assert inspect.isfunction(usage_function) and inspect.isfunction(execute_function) + # Set up signal handlers for sig in _SIGNALS: try: @@ -126,11 +125,7 @@ def _execute(module): #pylint: disable=unused-variable pass CMDLINE = Parser() - try: - module.usage(CMDLINE) - except AttributeError: - CMDLINE = None - raise + usage_function(CMDLINE) ######################################################################################################################## # Note that everything after this point will only be executed if the script is designed to operate against the library # @@ -240,7 +235,7 @@ def _execute(module): #pylint: disable=unused-variable sys.exit(return_code) try: - module.execute() + execute_function() except (run.MRtrixCmdError, run.MRtrixFnError) as exception: is_cmd = isinstance(exception, run.MRtrixCmdError) return_code = exception.returncode if is_cmd else 1 @@ -982,6 +977,21 @@ def flag_mutually_exclusive_options(self, options, required=False): #pylint: dis raise Exception('Parser.flagMutuallyExclusiveOptions() only accepts a list of strings') self._mutually_exclusive_option_groups.append( (options, required) ) + def add_subparsers(self): # pylint: disable=arguments-differ + # Import the command-line settings for all algorithms in the relevant sub-directories + # This is expected to be being called from the 'usage' module of the relevant command + module_name = os.path.dirname(inspect.getouterframes(inspect.currentframe())[1].filename).split(os.sep)[-1] + module = sys.modules['mrtrix3.commands.' + module_name] + base_parser = Parser(description='Base parser for construction of subparsers', parents=[self]) + subparsers = super().add_subparsers(title='Algorithm choices', + help='Select the algorithm to be used; ' + 'additional details and options become available once an algorithm is nominated. ' + 'Options are: ' + ', '.join(module.ALGORITHMS), + dest='algorithm') + for algorithm in module.ALGORITHMS: + algorithm_module = importlib.import_module('.' + algorithm, 'mrtrix3.commands.' + module_name) + algorithm_module.usage(base_parser, subparsers) + def parse_args(self, args=None, namespace=None): if not self._author: raise Exception('Script author MUST be set in script\'s usage() function') @@ -1116,11 +1126,11 @@ def underline(text, ignore_whitespace = True): if self._is_project: text = f'Version {self._git_version}' else: - text = f'MRtrix {__version__}' + text = f'MRtrix {version.VERSION}' text += ' ' * max(1, 40 - len(text) - int(len(self.prog)/2)) text += bold(self.prog) + '\n' if self._is_project: - text += f'using MRtrix3 {__version__}\n' + text += f'using MRtrix3 {version.VERSION}\n' text += '\n' text += ' ' + bold(self.prog) + f': {"external MRtrix3 project" if self._is_project else "part of the MRtrix3 package"}\n' text += '\n' @@ -1470,9 +1480,9 @@ def print_group_options(group): '__print_usage_rst__']) def print_version(self): - text = f'== {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.VERSION} ==\n' if self._is_project: - text += f'executing against MRtrix {__version__}\n' + text += f'executing against MRtrix {version.VERSION}\n' text += f'Author(s): {self._author}\n' text += f'{self._copyright}\n' sys.stdout.write(text) diff --git a/python/bin/5ttgen b/python/mrtrix3/commands/5ttgen/5ttgen.py old mode 100755 new mode 100644 similarity index 87% rename from python/bin/5ttgen rename to python/mrtrix3/commands/5ttgen/5ttgen.py index c504964bfb..c60582224c --- a/python/bin/5ttgen +++ b/python/mrtrix3/commands/5ttgen/5ttgen.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -15,10 +13,11 @@ # # For more details, see http://www.mrtrix.org/. +import importlib + 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('Generate a 5TT image suitable for ACT') @@ -51,15 +50,15 @@ def usage(cmdline): #pylint: disable=unused-variable 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) + cmdline.add_subparsers() def execute(): #pylint: disable=unused-variable - from mrtrix3 import algorithm, app, run #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import app, 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) + # Load module for the user-requested algorithm + alg = importlib.import_module(f'.{app.ARGS.algorithm}', 'mrtrix3.commands.5ttgen') app.activate_scratch_dir() @@ -70,9 +69,3 @@ def execute(): #pylint: disable=unused-variable app.warn('Generated image does not perfectly conform to 5TT format:') for line in stderr.splitlines(): app.warn(line) - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/mrtrix3/commands/5ttgen/__init__.py b/python/mrtrix3/commands/5ttgen/__init__.py new file mode 100644 index 0000000000..3f72263f4c --- /dev/null +++ b/python/mrtrix3/commands/5ttgen/__init__.py @@ -0,0 +1,17 @@ +# 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/. + +# pylint: disable=unused-variable +ALGORITHMS = ['freesurfer', 'fsl', 'gif', 'hsvs'] diff --git a/python/lib/mrtrix3/_5ttgen/freesurfer.py b/python/mrtrix3/commands/5ttgen/freesurfer.py similarity index 97% rename from python/lib/mrtrix3/_5ttgen/freesurfer.py rename to python/mrtrix3/commands/5ttgen/freesurfer.py index f7fc7af05e..14411bf4dd 100644 --- a/python/lib/mrtrix3/_5ttgen/freesurfer.py +++ b/python/mrtrix3/commands/5ttgen/freesurfer.py @@ -60,7 +60,7 @@ def execute(): #pylint: disable=unused-variable lut_output_file_name = 'FreeSurfer2ACT_sgm_amyg_hipp.txt' else: lut_output_file_name = 'FreeSurfer2ACT.txt' - lut_output_path = os.path.join(path.shared_data_path(), path.script_subdir_name(), lut_output_file_name) + lut_output_path = os.path.join(path.shared_data_path(), '5ttgen', lut_output_file_name) if not os.path.isfile(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})') diff --git a/python/lib/mrtrix3/_5ttgen/fsl.py b/python/mrtrix3/commands/5ttgen/fsl.py similarity index 100% rename from python/lib/mrtrix3/_5ttgen/fsl.py rename to python/mrtrix3/commands/5ttgen/fsl.py diff --git a/python/lib/mrtrix3/_5ttgen/gif.py b/python/mrtrix3/commands/5ttgen/gif.py similarity index 100% rename from python/lib/mrtrix3/_5ttgen/gif.py rename to python/mrtrix3/commands/5ttgen/gif.py diff --git a/python/lib/mrtrix3/_5ttgen/hsvs.py b/python/mrtrix3/commands/5ttgen/hsvs.py similarity index 99% rename from python/lib/mrtrix3/_5ttgen/hsvs.py rename to python/mrtrix3/commands/5ttgen/hsvs.py index d71eddf8ee..c9bfded485 100644 --- a/python/lib/mrtrix3/_5ttgen/hsvs.py +++ b/python/mrtrix3/commands/5ttgen/hsvs.py @@ -325,10 +325,10 @@ def execute(): #pylint: disable=unused-variable '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') + hipp_lut_file = os.path.join(path.shared_data_path(), '5ttgen', 'hsvs', 'HippSubfields.txt') check_file(hipp_lut_file) if hipp_subfield_has_amyg: - amyg_lut_file = os.path.join(path.shared_data_path(), path.script_subdir_name(), 'hsvs', 'AmygSubfields.txt') + amyg_lut_file = os.path.join(path.shared_data_path(), '5ttgen', 'hsvs', 'AmygSubfields.txt') check_file(amyg_lut_file) if app.ARGS.sgm_amyg_hipp: @@ -654,7 +654,7 @@ 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, wm_index, '-eq', aparc_image, gm_index, '-eq', '-add', '-', '|', + run.command(['mrcalc', aparc_image, str(wm_index), '-eq', aparc_image, str(gm_index), '-eq', '-add', '-', '|', 'voxel2mesh', '-', f'{hemi}cerebellum_all_init.vtk']) progress.increment() run.command(['mrcalc', aparc_image, gm_index, '-eq', '-', '|', diff --git a/python/mrtrix3/commands/CMakeLists.txt b/python/mrtrix3/commands/CMakeLists.txt new file mode 100644 index 0000000000..8861f055f1 --- /dev/null +++ b/python/mrtrix3/commands/CMakeLists.txt @@ -0,0 +1,110 @@ + +set(PYTHON_COMMANDS_INIT_FILE ${PROJECT_BINARY_DIR}/lib/mrtrix3/commands/__init__.py) + +file(GLOB PYTHON_ALL_COMMANDS_ROOT_PATHS + ${CMAKE_CURRENT_SOURCE_DIR}/* +) + +file(GLOB_RECURSE PYTHON_ALL_COMMANDS_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/*.py +) + +set(PYTHON_COMMAND_LIST "") +foreach(PYTHON_PATH ${PYTHON_ALL_COMMANDS_ROOT_PATHS}) + get_filename_component(CMDNAME ${PYTHON_PATH} NAME_WE) + if(NOT ${CMDNAME} STREQUAL "CMakeLists" AND NOT ${CMDNAME} STREQUAL "__init__") + list(APPEND PYTHON_COMMAND_LIST ${CMDNAME}) + endif() +endforeach() + +if(MRTRIX_PYTHON_SOFTLINK) + set(PYTHON_COMMANDS_TARGET_NAME "LinkPythonCommandFiles") + set(PYTHON_API_TARGET_NAME "LinkPythonCommandFiles") + set(PYTHON_COMMANDS_FUNCTION_NAME "create_symlink") +else() + set(PYTHON_COMMANDS_TARGET_NAME "CopyPythonCommandFiles") + set(PYTHON_API_TARGET_NAME "CopyPythonCommandFiles") + set(PYTHON_COMMANDS_FUNCTION_NAME "copy_if_different") +endif() + +add_custom_target(${PYTHON_COMMANDS_TARGET_NAME} ALL) +add_dependencies(${PYTHON_COMMANDS_TARGET_NAME} ${PYTHON_API_TARGET_NAME}) + +set(PYTHON_BUILD_COMMAND_FILES "") + +# Have to append commands to create all directories +# before commands to symlink files can appear +# Use presence of "__init__.py" as proxy for the need to construct a directory +foreach(PYTHON_SRC_PATH ${PYTHON_ALL_COMMANDS_FILES}) + get_filename_component(FILENAME ${PYTHON_SRC_PATH} NAME) + if(${FILENAME} STREQUAL "__init__.py") + file(RELATIVE_PATH REL_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${PYTHON_SRC_PATH}) + get_filename_component(DIRNAME ${REL_PATH} DIRECTORY) + add_custom_command( + TARGET ${PYTHON_COMMANDS_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/lib/mrtrix3/commands/${DIRNAME} + ) + endif() +endforeach() + +foreach(PYTHON_SRC_PATH ${PYTHON_ALL_COMMANDS_FILES}) + file(RELATIVE_PATH DST_RELPATH ${CMAKE_CURRENT_SOURCE_DIR} ${PYTHON_SRC_PATH}) + # Skip "commands/__init__.py"; + # this file will be written separately via execution of "commands/__init__.py.in" + if (${DST_RELPATH} STREQUAL "__init__.py") + continue() + endif() + set(DST_BUILDPATH ${PROJECT_BINARY_DIR}/lib/mrtrix3/commands/${DST_RELPATH}) + add_custom_command( + TARGET ${PYTHON_COMMANDS_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} -E ${PYTHON_COMMANDS_FUNCTION_NAME} ${PYTHON_SRC_PATH} ${DST_BUILDPATH} + DEPENDS ${PYTHON_SRC_PATH} + ) + get_filename_component(DST_INSTALLDIR ${DST_RELPATH} DIRECTORY) + install(FILES ${PYTHON_SRC_PATH} + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/mrtrix3/commands/${DST_INSTALLDIR} + ) + list(APPEND PYTHON_BUILD_COMMAND_FILES ${DST_PATH}) +endforeach() + +add_custom_target(MakePythonExecutables ALL) + +file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/temporary/python) +set(PYTHON_BIN_FILES "") +foreach(CMDNAME ${PYTHON_COMMAND_LIST}) + add_custom_command( + TARGET MakePythonExecutables + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND ${CMAKE_COMMAND} -DCMDNAME=${CMDNAME} -DBUILDDIR=${PROJECT_BINARY_DIR} -P ${CMAKE_SOURCE_DIR}/cmake/MakePythonExecutable.cmake + ) + list(APPEND PYTHON_BIN_FILES ${PROJECT_BINARY_DIR}/bin/${CMDNAME}) +endforeach() + +set_target_properties(MakePythonExecutables + PROPERTIES ADDITIONAL_CLEAN_FILES "${PROJECT_BINARY_DIR}/temporary/python" +) + +# We need to generate a list of MRtrix3 commands: +# function run.command() does different things if it is executing an MRtrix3 command vs. an external command, +# but unlike prior software versions we cannot simply interrogate the contents of the bin/ directory at runtime +add_custom_target(MakePythonCommandsInit ALL) +add_dependencies(MakePythonCommandsInit ${PYTHON_API_TARGET_NAME}) +add_custom_command( + TARGET MakePythonCommandsInit + COMMAND ${CMAKE_COMMAND} + -D DST=${PYTHON_COMMANDS_INIT_FILE} + -D SRC=${CMAKE_CURRENT_SOURCE_DIR}/__init__.py.in + -P ${PROJECT_SOURCE_DIR}/cmake/GenPythonCommandsLists.cmake + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} +) + +install(FILES ${PYTHON_BIN_FILES} + PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +install(FILES ${PYTHON_COMMANDS_INIT_FILE} + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/mrtrix3/commands/ +) diff --git a/python/mrtrix3/commands/__init__.py b/python/mrtrix3/commands/__init__.py new file mode 100644 index 0000000000..c633679d93 --- /dev/null +++ b/python/mrtrix3/commands/__init__.py @@ -0,0 +1,24 @@ +# 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/. + +# WARNING: Any content added to this file will NOT propagate to the built software! +# Contents for this file in the build directory are generated using file __init__.py.in + +#pylint: disable=unused-variable +EXECUTABLES_PATH = None +CPP_COMMANDS = [] +PYTHON_COMMANDS = [] +ALL_COMMANDS = CPP_COMMANDS + PYTHON_COMMANDS + diff --git a/python/mrtrix3/commands/__init__.py.in b/python/mrtrix3/commands/__init__.py.in new file mode 100644 index 0000000000..aa8cbe0f08 --- /dev/null +++ b/python/mrtrix3/commands/__init__.py.in @@ -0,0 +1,29 @@ +# 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/. + +import os + +EXECUTABLES_PATH = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir, os.pardir, 'bin')) + +CPP_COMMANDS = [ + @MRTRIX_CPP_COMMAND_LIST@ +] + +PYTHON_COMMANDS = [ + @MRTRIX_PYTHON_COMMAND_LIST@ +] + +ALL_COMMANDS = CPP_COMMANDS + PYTHON_COMMANDS + diff --git a/python/lib/mrtrix3/dwi2mask/3dautomask.py b/python/mrtrix3/commands/dwi2mask/3dautomask.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/3dautomask.py rename to python/mrtrix3/commands/dwi2mask/3dautomask.py diff --git a/python/mrtrix3/commands/dwi2mask/__init__.py b/python/mrtrix3/commands/dwi2mask/__init__.py new file mode 100644 index 0000000000..0fb40c1e56 --- /dev/null +++ b/python/mrtrix3/commands/dwi2mask/__init__.py @@ -0,0 +1,27 @@ +# 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/. + +# pylint: disable=unused-variable +ALGORITHMS = ['3dautomask', + 'ants', + 'b02template', + 'consensus', + 'fslbet', + 'hdbet', + 'legacy', + 'mean', + 'mtnorm', + 'synthstrip', + 'trace'] diff --git a/python/lib/mrtrix3/dwi2mask/ants.py b/python/mrtrix3/commands/dwi2mask/ants.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/ants.py rename to python/mrtrix3/commands/dwi2mask/ants.py diff --git a/python/lib/mrtrix3/dwi2mask/b02template.py b/python/mrtrix3/commands/dwi2mask/b02template.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/b02template.py rename to python/mrtrix3/commands/dwi2mask/b02template.py diff --git a/python/lib/mrtrix3/dwi2mask/consensus.py b/python/mrtrix3/commands/dwi2mask/consensus.py similarity index 98% rename from python/lib/mrtrix3/dwi2mask/consensus.py rename to python/mrtrix3/commands/dwi2mask/consensus.py index c585cf67d3..566e418d70 100644 --- a/python/lib/mrtrix3/dwi2mask/consensus.py +++ b/python/mrtrix3/commands/dwi2mask/consensus.py @@ -14,7 +14,8 @@ # For more details, see http://www.mrtrix.org/. from mrtrix3 import CONFIG, MRtrixError -from mrtrix3 import algorithm, app, run +from mrtrix3 import app, run +from . import ALGORITHMS NEEDS_MEAN_BZERO = False # pylint: disable=unused-variable DEFAULT_THRESHOLD = 0.501 @@ -54,7 +55,7 @@ def usage(base_parser, subparsers): #pylint: disable=unused-variable def execute(): #pylint: disable=unused-variable - algorithm_list = [item for item in algorithm.get_list() if item != 'consensus'] + algorithm_list = [item for item in ALGORITHMS if item != 'consensus'] app.debug(str(algorithm_list)) if app.ARGS.algorithms: diff --git a/python/bin/dwi2mask b/python/mrtrix3/commands/dwi2mask/dwi2mask.py old mode 100755 new mode 100644 similarity index 86% rename from python/bin/dwi2mask rename to python/mrtrix3/commands/dwi2mask/dwi2mask.py index ab805cc103..ca741db901 --- a/python/bin/dwi2mask +++ b/python/mrtrix3/commands/dwi2mask/dwi2mask.py @@ -1,6 +1,4 @@ -#!/usr/bin/python3 - -# Copyright (c) 2008-2023 the MRtrix3 contributors. +# 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 @@ -15,10 +13,12 @@ # # For more details, see http://www.mrtrix.org/. +import importlib + def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import algorithm, app, _version #pylint: disable=no-name-in-module, import-outside-toplevel + 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)' ' and Warda Syeda (wtsyeda@unimelb.edu.au)') @@ -30,23 +30,23 @@ def usage(cmdline): #pylint: disable=unused-variable ' 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') + f'https://mrtrix.readthedocs.io/en/{version.TAG}/dwi_preprocessing/masking.html') # General options #common_options = cmdline.add_argument_group('General dwi2mask options') app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory - algorithm.usage(cmdline) + cmdline.add_subparsers() 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, 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 - # Find out which algorithm the user has requested - alg = algorithm.get_module(app.ARGS.algorithm) + # Load module for the user-requested algorithm + alg = importlib.import_module(f'.{app.ARGS.algorithm}', 'mrtrix3.commands.dwi2mask') input_header = image.Header(app.ARGS.input) image.check_3d_nonunity(input_header) @@ -94,9 +94,3 @@ def execute(): #pylint: disable=unused-variable mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/lib/mrtrix3/dwi2mask/fslbet.py b/python/mrtrix3/commands/dwi2mask/fslbet.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/fslbet.py rename to python/mrtrix3/commands/dwi2mask/fslbet.py diff --git a/python/lib/mrtrix3/dwi2mask/hdbet.py b/python/mrtrix3/commands/dwi2mask/hdbet.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/hdbet.py rename to python/mrtrix3/commands/dwi2mask/hdbet.py diff --git a/python/lib/mrtrix3/dwi2mask/legacy.py b/python/mrtrix3/commands/dwi2mask/legacy.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/legacy.py rename to python/mrtrix3/commands/dwi2mask/legacy.py diff --git a/python/lib/mrtrix3/dwi2mask/mean.py b/python/mrtrix3/commands/dwi2mask/mean.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/mean.py rename to python/mrtrix3/commands/dwi2mask/mean.py diff --git a/python/lib/mrtrix3/dwi2mask/mtnorm.py b/python/mrtrix3/commands/dwi2mask/mtnorm.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/mtnorm.py rename to python/mrtrix3/commands/dwi2mask/mtnorm.py diff --git a/python/lib/mrtrix3/dwi2mask/synthstrip.py b/python/mrtrix3/commands/dwi2mask/synthstrip.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/synthstrip.py rename to python/mrtrix3/commands/dwi2mask/synthstrip.py diff --git a/python/lib/mrtrix3/dwi2mask/trace.py b/python/mrtrix3/commands/dwi2mask/trace.py similarity index 100% rename from python/lib/mrtrix3/dwi2mask/trace.py rename to python/mrtrix3/commands/dwi2mask/trace.py diff --git a/python/mrtrix3/commands/dwi2response/__init__.py b/python/mrtrix3/commands/dwi2response/__init__.py new file mode 100644 index 0000000000..8173a15e4b --- /dev/null +++ b/python/mrtrix3/commands/dwi2response/__init__.py @@ -0,0 +1,17 @@ +# 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/. + +# pylint: disable=unused-variable +ALGORITHMS = [ 'dhollander', 'fa', 'manual', 'msmt_5tt', 'tax', 'tournier' ] diff --git a/python/lib/mrtrix3/dwi2response/dhollander.py b/python/mrtrix3/commands/dwi2response/dhollander.py similarity index 100% rename from python/lib/mrtrix3/dwi2response/dhollander.py rename to python/mrtrix3/commands/dwi2response/dhollander.py diff --git a/python/bin/dwi2response b/python/mrtrix3/commands/dwi2response/dwi2response.py old mode 100755 new mode 100644 similarity index 89% rename from python/bin/dwi2response rename to python/mrtrix3/commands/dwi2response/dwi2response.py index 7135ffff02..276c5e51ac --- a/python/bin/dwi2response +++ b/python/mrtrix3/commands/dwi2response/dwi2response.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -15,10 +13,12 @@ # # For more details, see http://www.mrtrix.org/. +import importlib + def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import algorithm, app, _version #pylint: disable=no-name-in-module, import-outside-toplevel + 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)' ' and Thijs Dhollander (thijs.dhollander@gmail.com)') @@ -32,12 +32,12 @@ def usage(cmdline): #pylint: disable=unused-variable ' 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') + 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: \n' - f'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') @@ -60,7 +60,7 @@ def usage(cmdline): #pylint: disable=unused-variable app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory - algorithm.usage(cmdline) + cmdline.add_subparsers() @@ -69,10 +69,10 @@ 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, 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 - # Find out which algorithm the user has requested - alg = algorithm.get_module(app.ARGS.algorithm) + # Load module for the user-requested algorithm + alg = importlib.import_module(f'.{app.ARGS.algorithm}', 'mrtrix3.commands.dwi2response') # Sanitise some inputs, and get ready for data import if app.ARGS.lmax: @@ -135,12 +135,3 @@ def execute(): #pylint: disable=unused-variable # From here, the script splits depending on what estimation algorithm is being used alg.execute() - - - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/lib/mrtrix3/dwi2response/fa.py b/python/mrtrix3/commands/dwi2response/fa.py similarity index 100% rename from python/lib/mrtrix3/dwi2response/fa.py rename to python/mrtrix3/commands/dwi2response/fa.py diff --git a/python/lib/mrtrix3/dwi2response/manual.py b/python/mrtrix3/commands/dwi2response/manual.py similarity index 100% rename from python/lib/mrtrix3/dwi2response/manual.py rename to python/mrtrix3/commands/dwi2response/manual.py diff --git a/python/lib/mrtrix3/dwi2response/msmt_5tt.py b/python/mrtrix3/commands/dwi2response/msmt_5tt.py similarity index 100% rename from python/lib/mrtrix3/dwi2response/msmt_5tt.py rename to python/mrtrix3/commands/dwi2response/msmt_5tt.py diff --git a/python/lib/mrtrix3/dwi2response/tax.py b/python/mrtrix3/commands/dwi2response/tax.py similarity index 100% rename from python/lib/mrtrix3/dwi2response/tax.py rename to python/mrtrix3/commands/dwi2response/tax.py diff --git a/python/lib/mrtrix3/dwi2response/tournier.py b/python/mrtrix3/commands/dwi2response/tournier.py similarity index 100% rename from python/lib/mrtrix3/dwi2response/tournier.py rename to python/mrtrix3/commands/dwi2response/tournier.py diff --git a/python/mrtrix3/commands/dwibiascorrect/__init__.py b/python/mrtrix3/commands/dwibiascorrect/__init__.py new file mode 100644 index 0000000000..96baa29368 --- /dev/null +++ b/python/mrtrix3/commands/dwibiascorrect/__init__.py @@ -0,0 +1,17 @@ +# 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/. + +# pylint: disable=unused-variable +ALGORITHMS = [ 'ants', 'fsl', 'mtnorm' ] diff --git a/python/lib/mrtrix3/dwibiascorrect/ants.py b/python/mrtrix3/commands/dwibiascorrect/ants.py similarity index 100% rename from python/lib/mrtrix3/dwibiascorrect/ants.py rename to python/mrtrix3/commands/dwibiascorrect/ants.py diff --git a/python/bin/dwibiascorrect b/python/mrtrix3/commands/dwibiascorrect/dwibiascorrect.py old mode 100755 new mode 100644 similarity index 84% rename from python/bin/dwibiascorrect rename to python/mrtrix3/commands/dwibiascorrect/dwibiascorrect.py index 2360e21c31..ae8b457497 --- a/python/bin/dwibiascorrect +++ b/python/mrtrix3/commands/dwibiascorrect/dwibiascorrect.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -15,17 +13,19 @@ # # For more details, see http://www.mrtrix.org/. +import importlib + def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import algorithm, app, _version #pylint: disable=no-name-in-module, import-outside-toplevel + 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 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 ' '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') + f'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(), @@ -36,16 +36,16 @@ def usage(cmdline): #pylint: disable=unused-variable app.add_dwgrad_import_options(cmdline) # Import the command-line settings for all algorithms found in the relevant directory - algorithm.usage(cmdline) + cmdline.add_subparsers() 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, 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 - # Find out which algorithm the user has requested - alg = algorithm.get_module(app.ARGS.algorithm) + # Load module for the user-requested algorithm + alg = importlib.import_module(f'.{app.ARGS.algorithm}', 'mrtrix3.commands.dwibiascorrect') app.activate_scratch_dir() run.command(['mrconvert', app.ARGS.input, 'in.mif'] @@ -77,11 +77,3 @@ def execute(): #pylint: disable=unused-variable # From here, the script splits depending on what estimation algorithm is being used alg.execute() - - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/lib/mrtrix3/dwibiascorrect/fsl.py b/python/mrtrix3/commands/dwibiascorrect/fsl.py similarity index 100% rename from python/lib/mrtrix3/dwibiascorrect/fsl.py rename to python/mrtrix3/commands/dwibiascorrect/fsl.py diff --git a/python/lib/mrtrix3/dwibiascorrect/mtnorm.py b/python/mrtrix3/commands/dwibiascorrect/mtnorm.py similarity index 100% rename from python/lib/mrtrix3/dwibiascorrect/mtnorm.py rename to python/mrtrix3/commands/dwibiascorrect/mtnorm.py diff --git a/python/bin/dwibiasnormmask b/python/mrtrix3/commands/dwibiasnormmask.py old mode 100755 new mode 100644 similarity index 99% rename from python/bin/dwibiasnormmask rename to python/mrtrix3/commands/dwibiasnormmask.py index d4b9585a24..e7c2d3b052 --- a/python/bin/dwibiasnormmask +++ b/python/mrtrix3/commands/dwibiasnormmask.py @@ -1,6 +1,4 @@ -#!/usr/bin/python3 - -# Copyright (c) 2008-2023 the MRtrix3 contributors. +# 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 @@ -470,9 +468,3 @@ def msg(): mrconvert_keyval=app.ARGS.input, preserve_pipes=True, force=app.FORCE_OVERWRITE) - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/dwicat b/python/mrtrix3/commands/dwicat.py old mode 100755 new mode 100644 similarity index 98% rename from python/bin/dwicat rename to python/mrtrix3/commands/dwicat.py index 586cb86c2e..41b7b5da25 --- a/python/bin/dwicat +++ b/python/mrtrix3/commands/dwicat.py @@ -1,6 +1,4 @@ -#!/usr/bin/python3 - -# Copyright (c) 2008-2023 the MRtrix3 contributors. +# 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 @@ -15,8 +13,6 @@ # # For more details, see http://www.mrtrix.org/. - - import json, os, shutil @@ -244,10 +240,3 @@ def check_header(header): run.command(['mrconvert', 'result.mif', app.ARGS.output], mrconvert_keyval='result_final.json', force=app.FORCE_OVERWRITE) - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/dwifslpreproc b/python/mrtrix3/commands/dwifslpreproc.py old mode 100755 new mode 100644 similarity index 99% rename from python/bin/dwifslpreproc rename to python/mrtrix3/commands/dwifslpreproc.py index 61572e803c..f63464fa29 --- a/python/bin/dwifslpreproc +++ b/python/mrtrix3/commands/dwifslpreproc.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -22,7 +20,7 @@ def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app, _version #pylint: disable=no-name-in-module, import-outside-toplevel + 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') @@ -34,7 +32,7 @@ def usage(cmdline): #pylint: disable=unused-variable ' 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') + 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' @@ -44,7 +42,7 @@ def usage(cmdline): #pylint: disable=unused-variable ' 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') + 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;' @@ -353,7 +351,7 @@ 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 = app.UserPath(eddy_topup_entry[0][len('--topup='):]) + topup_file_userpath = eddy_topup_entry[0][len('--topup='):] eddy_manual_options = [entry for entry in eddy_manual_options if not entry.startswith('--topup=')] @@ -1604,12 +1602,3 @@ def scheme_times_match(one, two): + app.dwgrad_export_options(), mrconvert_keyval='output.json', force=app.FORCE_OVERWRITE) - - - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/dwigradcheck b/python/mrtrix3/commands/dwigradcheck.py old mode 100755 new mode 100644 similarity index 96% rename from python/bin/dwigradcheck rename to python/mrtrix3/commands/dwigradcheck.py index 98f53e56d5..afd8d59c05 --- a/python/bin/dwigradcheck +++ b/python/mrtrix3/commands/dwigradcheck.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -20,7 +18,7 @@ def usage(cmdline): #pylint: disable=unused-variable - from mrtrix3 import app, _version #pylint: disable=no-name-in-module, import-outside-toplevel + 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('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.') @@ -29,7 +27,7 @@ def usage(cmdline): #pylint: disable=unused-variable ' 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') + 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') @@ -223,8 +221,3 @@ def execute(): #pylint: disable=unused-variable + grad_import_option + grad_export_option, force=app.FORCE_OVERWRITE) - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/mrtrix3/commands/dwinormalise/__init__.py b/python/mrtrix3/commands/dwinormalise/__init__.py new file mode 100644 index 0000000000..3390ab0a1d --- /dev/null +++ b/python/mrtrix3/commands/dwinormalise/__init__.py @@ -0,0 +1,17 @@ +# 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/. + +# pylint: disable=unused-variable +ALGORITHMS = ['group', 'manual', 'mtnorm'] diff --git a/python/bin/dwinormalise b/python/mrtrix3/commands/dwinormalise/dwinormalise.py old mode 100755 new mode 100644 similarity index 79% rename from python/bin/dwinormalise rename to python/mrtrix3/commands/dwinormalise/dwinormalise.py index 52fb3a5cea..d96900feaf --- a/python/bin/dwinormalise +++ b/python/mrtrix3/commands/dwinormalise/dwinormalise.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -15,9 +13,11 @@ # # For more details, see http://www.mrtrix.org/. +import importlib + + 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' @@ -30,23 +30,15 @@ def usage(cmdline): #pylint: disable=unused-variable ' eg. "dwinormalise group -help".') # Import the command-line settings for all algorithms found in the relevant directory - algorithm.usage(cmdline) + cmdline.add_subparsers() def execute(): #pylint: disable=unused-variable - from mrtrix3 import algorithm, app #pylint: disable=no-name-in-module, import-outside-toplevel + from mrtrix3 import app #pylint: disable=no-name-in-module, import-outside-toplevel - # Find out which algorithm the user has requested - alg = algorithm.get_module(app.ARGS.algorithm) + # Load module for the user-requested algorithm + alg = importlib.import_module(f'.{app.ARGS.algorithm}', 'mrtrix3.commands.dwinormalise') # From here, the script splits depending on what algorithm is being used alg.execute() - - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/lib/mrtrix3/dwinormalise/group.py b/python/mrtrix3/commands/dwinormalise/group.py similarity index 100% rename from python/lib/mrtrix3/dwinormalise/group.py rename to python/mrtrix3/commands/dwinormalise/group.py diff --git a/python/lib/mrtrix3/dwinormalise/manual.py b/python/mrtrix3/commands/dwinormalise/manual.py similarity index 100% rename from python/lib/mrtrix3/dwinormalise/manual.py rename to python/mrtrix3/commands/dwinormalise/manual.py diff --git a/python/lib/mrtrix3/dwinormalise/mtnorm.py b/python/mrtrix3/commands/dwinormalise/mtnorm.py similarity index 100% rename from python/lib/mrtrix3/dwinormalise/mtnorm.py rename to python/mrtrix3/commands/dwinormalise/mtnorm.py diff --git a/python/bin/dwishellmath b/python/mrtrix3/commands/dwishellmath.py old mode 100755 new mode 100644 similarity index 96% rename from python/bin/dwishellmath rename to python/mrtrix3/commands/dwishellmath.py index 290fde3a1b..913853c6bc --- a/python/bin/dwishellmath +++ b/python/mrtrix3/commands/dwishellmath.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -83,9 +81,3 @@ def execute(): #pylint: disable=unused-variable mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/for_each b/python/mrtrix3/commands/for_each.py old mode 100755 new mode 100644 similarity index 97% rename from python/bin/for_each rename to python/mrtrix3/commands/for_each.py index e269c49278..8df3924238 --- a/python/bin/for_each +++ b/python/mrtrix3/commands/for_each.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -28,7 +26,7 @@ def usage(cmdline): #pylint: disable=unused-variable global CMDSPLIT - from mrtrix3 import _version #pylint: disable=no-name-in-module, import-outside-toplevel + 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' @@ -36,7 +34,7 @@ def usage(cmdline): #pylint: disable=unused-variable ' (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') + 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' @@ -355,7 +353,7 @@ def execute_parallel(): 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(f'{app.EXE_NAME}:\n') + sys.stderr.write(f'{app.EXEC_NAME}:\n') else: app.warn('Output from failed command:') for job in jobs: @@ -367,7 +365,7 @@ def execute_parallel(): else: app.warn(f'No output from command for input "{job.sub_in}" (return code = {job.returncode})') if fail_count > 1: - sys.stderr.write(f'{app.EXE_NAME}:\n') + sys.stderr.write(f'{app.EXEC_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]}') @@ -386,12 +384,3 @@ def execute_parallel(): app.console('No output from command for any inputs') app.console('Script reported successful completion for all inputs') - - - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/labelsgmfirst b/python/mrtrix3/commands/labelsgmfirst.py old mode 100755 new mode 100644 similarity index 92% rename from python/bin/labelsgmfirst rename to python/mrtrix3/commands/labelsgmfirst.py index 80fb75657f..140c5bcee6 --- a/python/bin/labelsgmfirst +++ b/python/mrtrix3/commands/labelsgmfirst.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -15,16 +13,6 @@ # # For more details, see http://www.mrtrix.org/. -# Script for 'repairing' a FreeSurfer parcellation image -# FreeSurfer's sub-cortical structure segmentation has been observed to be highly variable -# under scan-rescan conditions. This introduces unwanted variability into the connectome, -# as the parcellations don't overlap with the sub-cortical segmentations provided by -# FIRST for the sake of Anatomically-Constrained Tractography. This script determines the -# node indices that correspond to these structures, and replaces them with estimates -# derived from FIRST. - - - import math, os @@ -141,7 +129,7 @@ def execute(): #pylint: disable=unused-variable # This will map a structure name to an index sgm_lut = {} sgm_lut_file_name = 'FreeSurferSGM.txt' - sgm_lut_file_path = os.path.join(path.shared_data_path(), path.script_subdir_name(), sgm_lut_file_name) + sgm_lut_file_path = os.path.join(path.shared_data_path(), 'labelsgmfirst', sgm_lut_file_name) with open(sgm_lut_file_path, encoding='utf-8') as sgm_lut_file: for line in sgm_lut_file: line = line.rstrip() @@ -198,11 +186,3 @@ def execute(): #pylint: disable=unused-variable mrconvert_keyval=app.ARGS.parc, force=app.FORCE_OVERWRITE, preserve_pipes=True) - - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/mask2glass b/python/mrtrix3/commands/mask2glass.py old mode 100755 new mode 100644 similarity index 96% rename from python/bin/mask2glass rename to python/mrtrix3/commands/mask2glass.py index 439a9e2ade..f0f3129790 --- a/python/bin/mask2glass +++ b/python/mrtrix3/commands/mask2glass.py @@ -1,6 +1,4 @@ -#!/usr/bin/python3 - -# Copyright (c) 2008-2023 the MRtrix3 contributors. +# 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 @@ -105,7 +103,3 @@ def execute(): #pylint: disable=unused-variable mrconvert_keyval=app.ARGS.input, force=app.FORCE_OVERWRITE, preserve_pipes=True) - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/mrtrix_cleanup b/python/mrtrix3/commands/mrtrix_cleanup.py old mode 100755 new mode 100644 similarity index 98% rename from python/bin/mrtrix_cleanup rename to python/mrtrix3/commands/mrtrix_cleanup.py index ccec3f202e..140c218cf3 --- a/python/bin/mrtrix_cleanup +++ b/python/mrtrix3/commands/mrtrix_cleanup.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -159,9 +157,3 @@ def print_freed(): app.console('All items deleted successfully' + print_freed()) else: app.console('No files or directories found') - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/population_template b/python/mrtrix3/commands/population_template.py old mode 100755 new mode 100644 similarity index 98% rename from python/bin/population_template rename to python/mrtrix3/commands/population_template.py index 1f2fbefe8a..b835174835 --- a/python/bin/population_template +++ b/python/mrtrix3/commands/population_template.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -15,7 +13,6 @@ # # For more details, see http://www.mrtrix.org/. -# Generates an unbiased group-average template via image registration of images to a midway space. import json, math, os, re, shlex, shutil, sys DEFAULT_RIGID_SCALES = [0.3,0.4,0.6,0.8,1.0,1.0] @@ -796,12 +793,7 @@ def paths_to_file_uids(paths, prefix, 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 - - expected_commands = ['mrfilter', 'mrgrid', 'mrregister', 'mrtransform', 'mraverageheader', 'mrconvert', 'mrmath', 'transformcalc'] - for cmd in expected_commands: - if cmd not in EXE_LIST : - raise MRtrixError(f'Could not find "{cmd}" in bin/; binary commands not compiled?') + from mrtrix3 import MRtrixError, app, image, matrix, path, run #pylint: disable=no-name-in-module, import-outside-toplevel if not app.ARGS.type in REGISTRATION_MODES: raise MRtrixError(f'Registration type must be one of {REGISTRATION_MODES}; provided: "{app.ARGS.type}"') @@ -861,16 +853,16 @@ def execute(): #pylint: disable=unused-variable agg_measure = 'mean' if app.ARGS.aggregate is not None: if not app.ARGS.aggregate in AGGREGATION_MODES: - app.error(f'aggregation type must be one of {AGGREGATION_MODES}; provided: {app.ARGS.aggregate}') + raise MRtrixError(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 if agg_measure != 'weighted_mean': - app.error(f'aggregation weights require "-aggregate mean" option; provided: {app.ARGS.aggregate}') - if not os.path.isfile(app.ARGS.aggregation_weights): - app.error(f'aggregation weights file not found: {app.ARGS.aggregation_weights}') + raise MRtrixError(f'aggregation weights require "-aggregate mean" option; provided: {app.ARGS.aggregate}') + if not os.path.isfile(app.ARGS.aggregation_weights): + raise MRtrixError(f'aggregation weights file not found: {app.ARGS.aggregation_weights}') initial_alignment = app.ARGS.initial_alignment if initial_alignment not in INITIAL_ALIGNMENT: @@ -1731,9 +1723,3 @@ def nonlinear_msg(): run.command(['mrconvert', current_template_mask, app.ARGS.template_mask], mrconvert_keyval='NULL', force=app.FORCE_OVERWRITE) - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/bin/responsemean b/python/mrtrix3/commands/responsemean.py old mode 100755 new mode 100644 similarity index 97% rename from python/bin/responsemean rename to python/mrtrix3/commands/responsemean.py index ef0854cdb9..5a80662103 --- a/python/bin/responsemean +++ b/python/mrtrix3/commands/responsemean.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright (c) 2008-2024 the MRtrix3 contributors. # # This Source Code Form is subject to the terms of the Mozilla Public @@ -104,10 +102,3 @@ def execute(): #pylint: disable=unused-variable mean_coeffs = [ [ f/len(data) for f in line ] for line in weighted_sum_coeffs ] matrix.save_matrix(app.ARGS.output, mean_coeffs, force=app.FORCE_OVERWRITE) - - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member diff --git a/python/lib/mrtrix3/fsl.py b/python/mrtrix3/fsl.py similarity index 100% rename from python/lib/mrtrix3/fsl.py rename to python/mrtrix3/fsl.py diff --git a/python/lib/mrtrix3/image.py b/python/mrtrix3/image.py similarity index 100% rename from python/lib/mrtrix3/image.py rename to python/mrtrix3/image.py diff --git a/python/lib/mrtrix3/matrix.py b/python/mrtrix3/matrix.py similarity index 100% rename from python/lib/mrtrix3/matrix.py rename to python/mrtrix3/matrix.py diff --git a/python/lib/mrtrix3/path.py b/python/mrtrix3/path.py similarity index 87% rename from python/lib/mrtrix3/path.py rename to python/mrtrix3/path.py index de8264618b..51e53ecf4a 100644 --- a/python/lib/mrtrix3/path.py +++ b/python/mrtrix3/path.py @@ -17,7 +17,7 @@ -import ctypes, inspect, os, shutil, subprocess, time +import ctypes, os, shutil, subprocess, time @@ -45,27 +45,6 @@ def is_hidden(directory, filename): -# 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 -# need to access the shared data directory but not actually be using the algorithm module -def script_subdir_name(): #pylint: disable=unused-variable - from mrtrix3 import app #pylint: disable=import-outside-toplevel - frameinfo = inspect.stack()[-1] - try: - frame = frameinfo.frame - except AttributeError: # Prior to Version 3.5 - frame = frameinfo[0] - # If the script has been run through a softlink, we need the name of the original - # script in order to locate the additional data - name = os.path.basename(os.path.realpath(inspect.getfile(frame))) - if not name[0].isalpha(): - name = '_' + name - app.debug(name) - return name - - - # Find data in the relevant directory # Some scripts come with additional requisite data files; this function makes it easy to find them. # For data that is stored in a named sub-directory specifically for a particular script, this function will diff --git a/python/lib/mrtrix3/phaseencoding.py b/python/mrtrix3/phaseencoding.py similarity index 100% rename from python/lib/mrtrix3/phaseencoding.py rename to python/mrtrix3/phaseencoding.py diff --git a/python/lib/mrtrix3/run.py b/python/mrtrix3/run.py similarity index 98% rename from python/lib/mrtrix3/run.py rename to python/mrtrix3/run.py index b8dc3b3fb8..41ae71d337 100644 --- a/python/lib/mrtrix3/run.py +++ b/python/mrtrix3/run.py @@ -14,7 +14,8 @@ # For more details, see http://www.mrtrix.org/. 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 +from mrtrix3 import ANSI, COMMAND_HISTORY_STRING, MRtrixBaseError, MRtrixError +from mrtrix3.commands import ALL_COMMANDS, EXECUTABLES_PATH IOStream = collections.namedtuple('IOStream', 'handle filename') @@ -359,7 +360,7 @@ def quote_nonpipe(item): cmdstack[-1].extend([ '-append_property', 'command_history', COMMAND_HISTORY_STRING ]) for line in cmdstack: - is_mrtrix_exe = line[0] in EXE_LIST + is_mrtrix_exe = line[0] in ALL_COMMANDS if is_mrtrix_exe: line[0] = version_match(line[0]) if shared.get_num_threads() is not None: @@ -554,9 +555,9 @@ def exe_name(item): path = item elif item.endswith('.exe'): path = item - elif os.path.isfile(os.path.join(BIN_PATH, item)): + elif os.path.isfile(os.path.join(EXECUTABLES_PATH, item)): path = item - elif os.path.isfile(os.path.join(BIN_PATH, f'{item}.exe')): + elif os.path.isfile(os.path.join(EXECUTABLES_PATH, f'{item}.exe')): path = item + '.exe' elif shutil.which(item) is not None: path = item @@ -577,10 +578,10 @@ def exe_name(item): # which checks system32\ before PATH) def version_match(item): from mrtrix3 import app #pylint: disable=import-outside-toplevel - if not item in EXE_LIST: - app.debug(f'Command "{item}" not found in MRtrix3 bin/ directory') + if not item in ALL_COMMANDS: + app.debug(f'Command "{item}" not in list of MRtrix3 commands') return item - exe_path_manual = os.path.join(BIN_PATH, exe_name(item)) + exe_path_manual = os.path.join(EXECUTABLES_PATH, exe_name(item)) if os.path.isfile(exe_path_manual): app.debug(f'Version-matched executable for "{item}": {exe_path_manual}') return exe_path_manual diff --git a/python/lib/mrtrix3/sh.py b/python/mrtrix3/sh.py similarity index 100% rename from python/lib/mrtrix3/sh.py rename to python/mrtrix3/sh.py diff --git a/python/lib/mrtrix3/utils.py b/python/mrtrix3/utils.py similarity index 100% rename from python/lib/mrtrix3/utils.py rename to python/mrtrix3/utils.py diff --git a/python/mrtrix3/version.py b/python/mrtrix3/version.py new file mode 100644 index 0000000000..28b1ea85f7 --- /dev/null +++ b/python/mrtrix3/version.py @@ -0,0 +1,21 @@ +# 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/. + +# WARNING: Any content added to this file will NOT propagate to the built software! +# Contents for this file in the build directory are generated using file version.py.in + +#pylint: disable=unused-variable +VERSION = "NOT_VERSIONED" +TAG = "NOT_TAGGED" diff --git a/python/mrtrix3/version.py.in b/python/mrtrix3/version.py.in new file mode 100644 index 0000000000..7499769e05 --- /dev/null +++ b/python/mrtrix3/version.py.in @@ -0,0 +1,18 @@ +# 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/. + +VERSION = "@MRTRIX_VERSION@" +TAG = "@MRTRIX_GIT_TAG@" + diff --git a/run_pylint b/run_pylint index 9a4e3a1e50..a480c35b13 100755 --- a/run_pylint +++ b/run_pylint @@ -21,21 +21,8 @@ echo logging to \""$LOGFILE"\" # generate list of tests to run: tests="update_copyright" if [ $# == 0 ]; then - for lib_path in python/lib/mrtrix3/*; do - if [[ -d ${lib_path} && ! ( ${lib_path} == *"__pycache__"* ) ]]; then - for src_file in ${lib_path}/*.py; do - if [[ -f ${src_file} ]]; then - tests="$tests $src_file" - fi - done - elif [[ ${lib_path} == *.py ]]; then - tests="$tests $lib_path" - fi - done - for bin_path in python/bin/*; do - if [ -f ${bin_path} ] && $(head -n1 ${bin_path} | grep -q "#!/usr/bin/python3"); then - tests="$tests $bin_path" - fi + for filepath in $(find python/mrtrix3/ -name '*.py'); do + tests="$tests $filepath" done else tests="$@" diff --git a/share/mrtrix3/_5ttgen/FreeSurfer2ACT.txt b/share/mrtrix3/5ttgen/FreeSurfer2ACT.txt similarity index 100% rename from share/mrtrix3/_5ttgen/FreeSurfer2ACT.txt rename to share/mrtrix3/5ttgen/FreeSurfer2ACT.txt diff --git a/share/mrtrix3/_5ttgen/FreeSurfer2ACT_sgm_amyg_hipp.txt b/share/mrtrix3/5ttgen/FreeSurfer2ACT_sgm_amyg_hipp.txt similarity index 100% rename from share/mrtrix3/_5ttgen/FreeSurfer2ACT_sgm_amyg_hipp.txt rename to share/mrtrix3/5ttgen/FreeSurfer2ACT_sgm_amyg_hipp.txt diff --git a/share/mrtrix3/_5ttgen/hsvs/AmygSubfields.txt b/share/mrtrix3/5ttgen/hsvs/AmygSubfields.txt similarity index 100% rename from share/mrtrix3/_5ttgen/hsvs/AmygSubfields.txt rename to share/mrtrix3/5ttgen/hsvs/AmygSubfields.txt diff --git a/share/mrtrix3/_5ttgen/hsvs/HippSubfields.txt b/share/mrtrix3/5ttgen/hsvs/HippSubfields.txt similarity index 100% rename from share/mrtrix3/_5ttgen/hsvs/HippSubfields.txt rename to share/mrtrix3/5ttgen/hsvs/HippSubfields.txt From 71bc07cac1c34b0985b601c0cb6277d19a1dbe75 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sun, 9 Jun 2024 20:32:24 +1000 Subject: [PATCH 098/182] update_copyright: Changes to reflect post-cmake refactoring --- python/mrtrix3/commands/__init__.py | 1 - update_copyright | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/python/mrtrix3/commands/__init__.py b/python/mrtrix3/commands/__init__.py index c633679d93..8f77f73ede 100644 --- a/python/mrtrix3/commands/__init__.py +++ b/python/mrtrix3/commands/__init__.py @@ -21,4 +21,3 @@ CPP_COMMANDS = [] PYTHON_COMMANDS = [] ALL_COMMANDS = CPP_COMMANDS + PYTHON_COMMANDS - diff --git a/update_copyright b/update_copyright index c59453089c..68d008cd38 100755 --- a/update_copyright +++ b/update_copyright @@ -45,17 +45,16 @@ ProcessFile = namedtuple('ProcessFile', 'path keep_shebang comment') MRTRIX_ROOT = os.path.abspath(os.path.dirname(__file__)) APP_CPP_PATH = os.path.join(MRTRIX_ROOT, 'core', 'app.cpp') APP_CPP_STRING = 'const char *COPYRIGHT =' -APP_PY_PATH = os.path.join(MRTRIX_ROOT, 'python', 'lib', 'mrtrix3', 'app.py') +APP_PY_PATH = os.path.join(MRTRIX_ROOT, 'python', 'mrtrix3', 'app.py') APP_PY_STRING = '_DEFAULT_COPYRIGHT =' SEARCH_PATHS = [ SearchPath('.', False, True, '#', [], ['mrtrix-mrview.desktop']), - SearchPath(os.path.join('python', 'bin'), False, True, '#', ['.py'], []), SearchPath('cmd', False, False, '*', ['.cpp'], []), SearchPath('core', True, False, '*', ['.cpp', '.h'], [os.path.join('file', 'json.h'), os.path.join('file', 'nifti1.h'), 'version.h']), SearchPath('docs', False, True, '#', [], []), SearchPath('matlab', True, False, '%', ['.m'], []), - SearchPath(os.path.join('python', 'lib'), True, False, '#', ['.py'], [os.path.join('mrtrix3', '_version.py.in')]), + SearchPath('python', True, False, '#', ['.py'], [os.path.join('mrtrix3', 'version.py.in'), os.path.join('mrtrix3', 'commands', '__init__.py.in')]), SearchPath(os.path.join('share', 'mrtrix3'), True, False, '#', ['.txt'], []), SearchPath('src', True, False, '*', ['.cpp', '.h'], []), SearchPath(os.path.join('testing', 'tools'), False, False, '*', ['.cpp'], []), From 9b3de4d4defc3c7572bcd3ab9f214b72ce91abf3 Mon Sep 17 00:00:00 2001 From: MRtrixBot Date: Sun, 9 Jun 2024 20:33:33 +1000 Subject: [PATCH 099/182] population_template: Split source code between files --- .../commands/population_template/__init__.py | 49 + .../commands/population_template/contrasts.py | 107 +++ .../execute.py} | 845 +----------------- .../commands/population_template/input.py | 141 +++ .../commands/population_template/usage.py | 288 ++++++ .../commands/population_template/utils.py | 297 ++++++ 6 files changed, 924 insertions(+), 803 deletions(-) create mode 100644 python/mrtrix3/commands/population_template/__init__.py create mode 100644 python/mrtrix3/commands/population_template/contrasts.py rename python/mrtrix3/commands/{population_template.py => population_template/execute.py} (53%) create mode 100644 python/mrtrix3/commands/population_template/input.py create mode 100644 python/mrtrix3/commands/population_template/usage.py create mode 100644 python/mrtrix3/commands/population_template/utils.py diff --git a/python/mrtrix3/commands/population_template/__init__.py b/python/mrtrix3/commands/population_template/__init__.py new file mode 100644 index 0000000000..e67ed1d462 --- /dev/null +++ b/python/mrtrix3/commands/population_template/__init__.py @@ -0,0 +1,49 @@ +# 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/. + +# pylint: disable=unused-variable + +DEFAULT_RIGID_SCALES = [0.3,0.4,0.6,0.8,1.0,1.0] +DEFAULT_RIGID_LMAX = [2,2,2,4,4,4] +DEFAULT_AFFINE_SCALES = [0.3,0.4,0.6,0.8,1.0,1.0] +DEFAULT_AFFINE_LMAX = [2,2,2,4,4,4] + +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,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'] + +LINEAR_ESTIMATORS = ['l1', 'l2', 'lp', 'none'] + +AGGREGATION_MODES = ['mean', 'median'] + +LINEAR_ESTIMATORS = ['l1', 'l2', 'lp', 'none'] + +INITIAL_ALIGNMENT = ['mass', 'robust_mass', 'geometric', 'none'] + +LEAVE_ONE_OUT = ['0', '1', 'auto'] + +IMAGEEXT = ['mif', 'nii', 'mih', 'mgh', 'mgz', 'img', 'hdr'] diff --git a/python/mrtrix3/commands/population_template/contrasts.py b/python/mrtrix3/commands/population_template/contrasts.py new file mode 100644 index 0000000000..d4450b5897 --- /dev/null +++ b/python/mrtrix3/commands/population_template/contrasts.py @@ -0,0 +1,107 @@ +# 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/. + +import os + +from mrtrix3 import MRtrixError +from mrtrix3 import app + + + +class Contrasts: # pylint: disable=unused-variable + """ + Class that parses arguments and holds information specific to each image contrast + + Attributes + ---------- + suff: list of str + identifiers used for contrast-specific filenames and folders ['_c0', '_c1', ...] + + names: list of str + derived from constrast-specific input folder + + templates_out: list of str + full path to output templates + + templates: list of str + holds current template names during registration + + n_volumes: list of int + number of volumes in each contrast + + fod_reorientation: list of bool + whether to perform FOD reorientation with mrtransform + + isfinite_count: list of str + filenames of images holding (weighted) number of finite-valued voxels across all images + + mc_weight_: list of floats + contrast-specific weight used during initialisation / registration + + _weight_option: list of str + weight option to be passed to mrregister, = {'initial_alignment', 'rigid', 'affine', 'nl'} + + n_contrasts: int + + """ + + + def __init__(self): + n_contrasts = len(app.ARGS.input_dir) + 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 = 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)] + self.mc_weight_affine = [None for _ in range(self.n_contrasts)] + self.mc_weight_nl = [None for _ in range(self.n_contrasts)] + self.initial_alignment_weight_option = [None for _ in range(self.n_contrasts)] + self.rigid_weight_option = [None for _ in range(self.n_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 = [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(f'mc_weight_{mode}', None) + if opt: + if n_contrasts == 1: + raise MRtrixError(f'mc_weight_{mode} requires multiple input contrasts') + if len(opt) != n_contrasts: + raise MRtrixError(f'mc_weight_{mode} needs to be defined for each contrast') + 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(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) + + def __repr__(self, *args, **kwargs): + text = '' + for cid in range(self.n_contrasts): + text += f'\tcontrast: {self.names[cid]}, suffix: {self.suff[cid]}\n' + return text diff --git a/python/mrtrix3/commands/population_template.py b/python/mrtrix3/commands/population_template/execute.py similarity index 53% rename from python/mrtrix3/commands/population_template.py rename to python/mrtrix3/commands/population_template/execute.py index b835174835..510705bf05 100644 --- a/python/mrtrix3/commands/population_template.py +++ b/python/mrtrix3/commands/population_template/execute.py @@ -13,787 +13,25 @@ # # For more details, see http://www.mrtrix.org/. -import json, math, os, re, shlex, shutil, sys - -DEFAULT_RIGID_SCALES = [0.3,0.4,0.6,0.8,1.0,1.0] -DEFAULT_RIGID_LMAX = [2,2,2,4,4,4] -DEFAULT_AFFINE_SCALES = [0.3,0.4,0.6,0.8,1.0,1.0] -DEFAULT_AFFINE_LMAX = [2,2,2,4,4,4] - -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,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'] - -LINEAR_ESTIMATORS = ['l1', 'l2', 'lp', 'none'] - -AGGREGATION_MODES = ['mean', 'median'] - -LINEAR_ESTIMATORS = ['l1', 'l2', 'lp', 'none'] - -INITIAL_ALIGNMENT = ['mass', 'robust_mass', 'geometric', 'none'] - -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 #pylint: disable=no-name-in-module, import-outside-toplevel - 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_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 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).') - 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', - 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, - 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' - 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})') - - class SequenceDirectoryOut(app.Parser.CustomTypeBase): - def __call__(self, input_value): - return [cmdline.make_userpath_object(app.Parser._UserDirOutPathExtras, item) # pylint: disable=protected-access \ - 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', - 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 combinations 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') - options.add_argument('-transformed_dir', - 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,' - ' 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' - ' 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', - 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' - ' 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', - 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 - - - -def abspath(arg, *args): - return os.path.abspath(os.path.join(arg, *args)) - - -def copy(src, dst, follow_symlinks=True): - """Copy data but do not set mode bits. Return the file's destination. - - mimics shutil.copy but without setting mode bits as shutil.copymode can fail on exotic mounts - (observed on cifs with file_mode=0777). - """ - if os.path.isdir(dst): - dst = os.path.join(dst, os.path.basename(src)) - if sys.version_info[0] > 2: - shutil.copyfile(src, dst, follow_symlinks=follow_symlinks) # pylint: disable=unexpected-keyword-arg - else: - shutil.copyfile(src, dst) - return dst - - -def check_linear_transformation(transformation, cmd, max_scaling=0.5, max_shear=0.2, max_rot=None, pause_on_warn=True): - from mrtrix3 import app, run, utils #pylint: disable=no-name-in-module, import-outside-toplevel - if max_rot is None: - max_rot = 2 * math.pi - - good = True - 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(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(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(f'large shear ({shear}) in {transformation}') - good = False - rot_angle = float(data['angle_axis'][0]) - if abs(rot_angle) > max_rot: - app.warn(f'large rotation ({rot_angle}) in {transformation}') - good = False - - if not good: - newcmd = [] - what = '' - init_rotation_found = False - skip = 0 - for element in cmd.split(): - if skip: - skip -= 1 - continue - if '_init_rotation' in element: - init_rotation_found = True - if '_init_matrix' in element: - skip = 1 - continue - if 'affine_scale' in element: - assert what != 'rigid' - what = 'affine' - elif 'rigid_scale' in element: - assert what != 'affine' - what = 'rigid' - newcmd.append(element) - newcmd = ' '.join(newcmd) - if not init_rotation_found: - app.console('replacing the transformation obtained with:') - app.console(cmd) - if what: - 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{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 - - -def aggregate(inputs, output, contrast_idx, mode, force=True): - from mrtrix3 import MRtrixError, run # pylint: disable=no-name-in-module, import-outside-toplevel - - images = [inp.ims_transformed[contrast_idx] for inp in inputs] - if mode == 'mean': - run.command(['mrmath', images, 'mean', '-keep_unary_axes', output], force=force) - elif mode == 'median': - run.command(['mrmath', images, 'median', '-keep_unary_axes', output], force=force) - elif mode == 'weighted_mean': - weights = [inp.aggregation_weight for inp in inputs] - assert not any(w is None for w in weights), weights - wsum = sum(float(w) for w in weights) - cmd = ['mrcalc'] - if wsum <= 0: - 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 += [f'{wsum:.16f}', '-div', output] - run.command(cmd, force=force) - else: - raise MRtrixError(f'aggregation mode {mode} not understood') - - -def inplace_nan_mask(images, masks): - from mrtrix3 import run # pylint: disable=no-name-in-module, import-outside-toplevel - 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, f'__{os.path.split(image)[1]}') - run.command(f'mrcalc {mask} {image} nan -if {masked}', force=True) - run.function(shutil.move, masked, image) - - -def calculate_isfinite(inputs, contrasts): - from mrtrix3 import run, path # pylint: disable=no-name-in-module, import-outside-toplevel - agg_weights = [float(inp.aggregation_weight) for inp in inputs if inp.aggregation_weight is not None] - for cid in range(contrasts.n_contrasts): - for inp in inputs: - if contrasts.n_volumes[cid] > 0: - cmd = f'mrconvert {inp.ims_transformed[cid]} -coord 3 0 - | ' \ - f'mrcalc - -finite' - else: - cmd = f'mrcalc {inp.ims_transformed[cid]} -finite' - if inp.aggregation_weight: - 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(f'isfinite{contrasts.suff[cid]}'), 'sum'] - if agg_weights: - 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) - - -def get_common_postfix(file_list): - return os.path.commonprefix([i[::-1] for i in file_list])[::-1] - - -def get_common_prefix(file_list): - return os.path.commonprefix(file_list) - - -class Contrasts: - """ - Class that parses arguments and holds information specific to each image contrast - - Attributes - ---------- - suff: list of str - identifiers used for contrast-specific filenames and folders ['_c0', '_c1', ...] - - names: list of str - derived from constrast-specific input folder - - templates_out: list of str - full path to output templates - - templates: list of str - holds current template names during registration - - n_volumes: list of int - number of volumes in each contrast - - fod_reorientation: list of bool - whether to perform FOD reorientation with mrtransform - - isfinite_count: list of str - filenames of images holding (weighted) number of finite-valued voxels across all images - - mc_weight_: list of floats - contrast-specific weight used during initialisation / registration - - _weight_option: list of str - weight option to be passed to mrregister, = {'initial_alignment', 'rigid', 'affine', 'nl'} - - n_contrasts: int - - """ - - - def __init__(self): - from mrtrix3 import MRtrixError, app # pylint: disable=no-name-in-module, import-outside-toplevel - - n_contrasts = len(app.ARGS.input_dir) - 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 = 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)] - self.mc_weight_affine = [None for _ in range(self.n_contrasts)] - self.mc_weight_nl = [None for _ in range(self.n_contrasts)] - self.initial_alignment_weight_option = [None for _ in range(self.n_contrasts)] - self.rigid_weight_option = [None for _ in range(self.n_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 = [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(f'mc_weight_{mode}', None) - if opt: - if n_contrasts == 1: - raise MRtrixError(f'mc_weight_{mode} requires multiple input contrasts') - if len(opt) != n_contrasts: - raise MRtrixError(f'mc_weight_{mode} needs to be defined for each contrast') - 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(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) - - def __repr__(self, *args, **kwargs): - text = '' - for cid in range(self.n_contrasts): - text += f'\tcontrast: {self.names[cid]}, suffix: {self.suff[cid]}\n' - return text - - -class Input: - """ - Class that holds input information specific to a single image (multiple contrasts) - - Attributes - ---------- - uid: str - unique identifier for these input image(s), does not contain spaces - - ims_path: list of str - full path to input images, shell quoted OR paths to cached file if cache_local was called - - msk_path: str - full path to input mask, shell quoted OR path to cached file if cache_local was called - - ims_filenames : list of str - for each contrast the input file paths stripped of their respective directories. Used for final output only. - - msk_filename: str - as ims_filenames - - ims_transformed: list of str - input_transformed/.mif - - msk_transformed: list of str - mask_transformed/.mif - - aggregation_weight: float - weights used in image aggregation that forms the template. Has to be normalised across inputs. - - _im_directories : list of str - full path to user-provided input directories containing the input images, one for each contrast - - _msk_directory: str - full path to user-provided mask directory - - _local_ims: list of str - path to cached input images - - _local_msk: str - path to cached input mask - - Methods - ------- - cache_local() - copy files into folders in current working directory. modifies _local_ims and _local_msk - - """ - def __init__(self, uid, filenames, directories, contrasts, mask_filename='', mask_directory=''): - self.contrasts = contrasts - - self.uid = uid - assert self.uid, "UID empty" - assert self.uid.count(' ') == 0, f'UID "{self.uid}" contains whitespace' - - assert len(directories) == len(filenames) - self.ims_filenames = filenames - self._im_directories = directories - - self.msk_filename = mask_filename - self._msk_directory = mask_directory - - n_contrasts = len(contrasts) - - 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 - - self._local_ims = [] - self._local_msk = None - - def __repr__(self, *args, **kwargs): - text = '\nInput [' - for key in sorted([k for k in self.__dict__ if not k.startswith('_')]): - text += f'\n\t{key}: {self.__dict__[key]}' - text += '\n]' - return text - - def info(self): - message = [f'input: {self.uid}'] - if self.aggregation_weight: - message += [f'agg weight: {self.aggregation_weight}'] - for csuff, fname in zip(self.contrasts, self.ims_filenames): - message += [f'{(csuff + ": ") if csuff else ""}: "{fname}"'] - if self.msk_filename: - message += [f'mask: {self.msk_filename}'] - return ', '.join(message) - - def cache_local(self): - from mrtrix3 import run # pylint: disable=no-name-in-module, import-outside-toplevel - contrasts = self.contrasts - for cid, csuff in enumerate(contrasts): - 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: - 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') - - def get_ims_path(self, quoted=True): - """ return path to input images """ - if self._local_ims: - return self._local_ims - 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 """ - if self._local_msk: - return self._local_msk - 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) - - -def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whitespace_repl='_'): - """ - matches input images across contrasts and pair them with masks. - extracts unique identifiers from mask and image filenames by stripping common pre and postfix (per contrast and for masks) - unique identifiers contain ASCII letters, numbers and '_' but no whitespace which is replaced by whitespace_repl - - in_files: list of lists - the inner list holds filenames specific to a contrast - - mask_files: - can be empty - - returns list of Input - - checks: 3d_nonunity - TODO check if no common grid & trafo across contrasts (only relevant for robust init?) - - """ - 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): - """ strip pre and postfix from filename, replace whitespace characters """ - uid_path = {} - uids = [] - for path in paths: - 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(f'No uniquely identifiable part of filename "{path}" ' - 'after prefix and postfix substitution ' - 'with prefix "{prefix}" and postfix "{postfix}"') - app.debug(f'UID mapping: "{path}" --> "{uid}"') - if uid in uid_path: - 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 - - # mask uids - mask_uids = [] - if mask_files: - mask_common_postfix = get_common_postfix(mask_files) - if not mask_common_postfix: - raise MRtrixError('mask filenames do not have a common postfix') - 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(f'mask uids: {mask_uids}') - - # images uids - common_postfix = [get_common_postfix(files) for files in in_files] - common_prefix = [get_common_prefix(files) for files in in_files] - # xcontrast_xsubject_pre_postfix: prefix and postfix of the common part across contrasts and subjects, - # without image extensions and leading or trailing '_' or '-' - 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(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%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(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 = [app.ARGS.input_dir[0]] - if len(contrasts) > 1: - for cid in range(1, len(contrasts)): - 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(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: - 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=app.ARGS.mask_dir)) - else: - inputs.append(Input(uid, fnames, dirs, contrasts)) - - # parse aggregation weights and match to inputs - if f_agg_weight: - import csv # pylint: disable=import-outside-toplevel - try: - with open(f_agg_weight, 'r', encoding='utf-8') as fweights: - agg_weights = dict((row[0].lstrip().rstrip(), row[1]) for row in csv.reader(fweights, delimiter=',', quotechar='#')) - except UnicodeDecodeError: - with open(f_agg_weight, 'r', encoding='utf-8') as fweights: - reader = csv.reader(fweights.read().decode('utf-8', errors='replace'), delimiter=',', quotechar='#') - agg_weights = dict((row[0].lstrip().rstrip(), row[1]) for row in reader) - pref = '^' + re.escape(get_common_prefix(list(agg_weights.keys()))) - suff = re.escape(get_common_postfix(list(agg_weights.keys()))) + '$' - for key in agg_weights.keys(): - agg_weights[re.sub(suff, '', re.sub(pref, '', key))] = agg_weights.pop(key).strip() - - for inp in inputs: - if inp.uid not in agg_weights: - raise MRtrixError(f'aggregation weight not found for {inp.uid}') - inp.aggregation_weight = agg_weights[inp.uid] - 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(f'Sum of aggregation weights is not positive: {weights}') - if any(w < 0 for w in weights): - app.warn(f'Negative aggregation weights: {weights}') - - return inputs, xcontrast_xsubject_pre_postfix - +import json, os, shutil +from mrtrix3 import MRtrixError +from mrtrix3 import app, image, matrix, path, run +from .contrasts import Contrasts +from . import utils +from . import AGGREGATION_MODES, \ + DEFAULT_AFFINE_LMAX, \ + DEFAULT_AFFINE_SCALES, \ + DEFAULT_NL_LMAX, \ + DEFAULT_NL_NITER, \ + DEFAULT_NL_SCALES, \ + DEFAULT_RIGID_LMAX, \ + DEFAULT_RIGID_SCALES, \ + INITIAL_ALIGNMENT, \ + LEAVE_ONE_OUT, \ + REGISTRATION_MODES def execute(): #pylint: disable=unused-variable - from mrtrix3 import MRtrixError, app, image, matrix, path, run #pylint: disable=no-name-in-module, import-outside-toplevel if not app.ARGS.type in REGISTRATION_MODES: raise MRtrixError(f'Registration type must be one of {REGISTRATION_MODES}; provided: "{app.ARGS.type}"') @@ -891,7 +129,7 @@ def execute(): #pylint: disable=unused-variable if nanmask_input and not use_masks: 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) + ins, xcontrast_xsubject_pre_postfix = utils.parse_input_files(in_files, mask_files, cns, agg_weights) leave_one_out = 'auto' if app.ARGS.leave_one_out is not None: @@ -1182,9 +420,9 @@ def execute(): #pylint: disable=unused-variable 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, f'average_header{cns.suff[cid]}.mif') + run.function(utils.copy, avh3d, f'average_header{cns.suff[cid]}.mif') elif cns.n_volumes[cid] == 1: - run.function(copy, avh4d, f'average_header{cns.suff[cid]}.mif') + run.function(utils.copy, avh4d, f'average_header{cns.suff[cid]}.mif') else: run.command(['mrcat', [avh3d] * cns.n_volumes[cid], '-axis', '3', f'average_header{cns.suff[cid]}.mif']) run.function(os.remove, avh3d) @@ -1212,18 +450,18 @@ def execute(): #pylint: disable=unused-variable progress.done() if nanmask_input: - inplace_nan_mask([inp.ims_transformed[cid] for inp in ins for cid in range(n_contrasts)], - [inp.msk_transformed for inp in ins for cid in range(n_contrasts)]) + utils.inplace_nan_mask([inp.ims_transformed[cid] for inp in ins for cid in range(n_contrasts)], + [inp.msk_transformed for inp in ins for cid in range(n_contrasts)]) if leave_one_out: - calculate_isfinite(ins, cns) + utils.calculate_isfinite(ins, cns) if not dolinear: for inp in ins: 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, f'average_header{cns.suff[0]}.mif', 'average_header.mif') + run.function(utils.copy, f'average_header{cns.suff[0]}.mif', 'average_header.mif') else: progress = app.ProgressBar('Performing initial rigid registration to template', len(ins)) @@ -1333,15 +571,15 @@ def execute(): #pylint: disable=unused-variable progress.done() if nanmask_input: - inplace_nan_mask([inp.ims_transformed[cid] for inp in ins for cid in range(n_contrasts)], - [inp.msk_transformed for inp in ins for cid in range(n_contrasts)]) + utils.inplace_nan_mask([inp.ims_transformed[cid] for inp in ins for cid in range(n_contrasts)], + [inp.msk_transformed for inp in ins for cid in range(n_contrasts)]) if leave_one_out: - calculate_isfinite(ins, cns) + utils.calculate_isfinite(ins, cns) cns.templates = [f'initial_template{contrast}.mif' for contrast in cns.suff] for cid in range(n_contrasts): - aggregate(ins, f'initial_template{cns.suff[cid]}.mif', cid, agg_measure) + utils.aggregate(ins, f'initial_template{cns.suff[cid]}.mif', cid, agg_measure) if cns.n_volumes[cid] == 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') @@ -1349,7 +587,8 @@ def execute(): #pylint: disable=unused-variable # Optimise template with linear registration if not dolinear: for inp in ins: - run.function(copy, os.path.join('linear_transforms_initial', f'{inp.uid}.txt'), + run.function(utils.copy, + os.path.join('linear_transforms_initial', f'{inp.uid}.txt'), os.path.join('linear_transforms', f'{inp.uid}.txt')) else: level = 0 @@ -1440,9 +679,9 @@ def linear_msg(): output_option + \ mrregister_log_option run.command(command, force=True) - check_linear_transformation(os.path.join(f'linear_transforms_{level:02d}', f'{inp.uid}.txt'), - command, - pause_on_warn=do_pause_on_warn) + utils.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: run.function(os.remove, im_temp) @@ -1499,7 +738,7 @@ def linear_msg(): for inp in ins: 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, + run.function(utils.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) @@ -1535,24 +774,24 @@ def linear_msg(): progress.increment() if nanmask_input: - inplace_nan_mask([inp.ims_transformed[cid] for inp in ins for cid in range(n_contrasts)], - [inp.msk_transformed for inp in ins for cid in range(n_contrasts)]) + utils.inplace_nan_mask([inp.ims_transformed[cid] for inp in ins for cid in range(n_contrasts)], + [inp.msk_transformed for inp in ins for cid in range(n_contrasts)]) if leave_one_out: - calculate_isfinite(ins, cns) + utils.calculate_isfinite(ins, cns) for cid in range(n_contrasts): if level > 0 and app.ARGS.delete_temporary_files: os.remove(cns.templates[cid]) cns.templates[cid] = f'linear_template{level:02d}{cns.suff[cid]}.mif' - aggregate(ins, cns.templates[cid], cid, agg_measure) + utils.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(f'mrconvert tmp.mif {cns.templates[cid]} -axes 0,1,2,-1') run.function(os.remove, 'tmp.mif') for entry in os.listdir(f'linear_transforms_{level:02d}'): - run.function(copy, + run.function(utils.copy, os.path.join(f'linear_transforms_{level:02d}', entry), os.path.join('linear_transforms', entry)) progress.done() @@ -1642,17 +881,17 @@ def nonlinear_msg(): progress.increment(nonlinear_msg()) if nanmask_input: - inplace_nan_mask([_inp.ims_transformed[cid] for _inp in ins for cid in range(n_contrasts)], - [_inp.msk_transformed for _inp in ins for cid in range(n_contrasts)]) + utils.inplace_nan_mask([_inp.ims_transformed[cid] for _inp in ins for cid in range(n_contrasts)], + [_inp.msk_transformed for _inp in ins for cid in range(n_contrasts)]) if leave_one_out: - calculate_isfinite(ins, cns) + utils.calculate_isfinite(ins, cns) for cid in range(n_contrasts): if level > 0 and app.ARGS.delete_temporary_files: os.remove(cns.templates[cid]) cns.templates[cid] = f'nl_template{level:02d}{cns.suff[cid]}.mif' - aggregate(ins, cns.templates[cid], cid, agg_measure) + utils.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(f'mrconvert tmp.mif {cns.templates[cid]} -axes 0,1,2,-1') diff --git a/python/mrtrix3/commands/population_template/input.py b/python/mrtrix3/commands/population_template/input.py new file mode 100644 index 0000000000..67c66d7b44 --- /dev/null +++ b/python/mrtrix3/commands/population_template/input.py @@ -0,0 +1,141 @@ +# 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/. + +import os, shlex + +class Input: # pylint: disable=unused-variable + """ + Class that holds input information specific to a single image (multiple contrasts) + + Attributes + ---------- + uid: str + unique identifier for these input image(s), does not contain spaces + + ims_path: list of str + full path to input images, shell quoted OR paths to cached file if cache_local was called + + msk_path: str + full path to input mask, shell quoted OR path to cached file if cache_local was called + + ims_filenames : list of str + for each contrast the input file paths stripped of their respective directories. Used for final output only. + + msk_filename: str + as ims_filenames + + ims_transformed: list of str + input_transformed/.mif + + msk_transformed: list of str + mask_transformed/.mif + + aggregation_weight: float + weights used in image aggregation that forms the template. Has to be normalised across inputs. + + _im_directories : list of str + full path to user-provided input directories containing the input images, one for each contrast + + _msk_directory: str + full path to user-provided mask directory + + _local_ims: list of str + path to cached input images + + _local_msk: str + path to cached input mask + + Methods + ------- + cache_local() + copy files into folders in current working directory. modifies _local_ims and _local_msk + + """ + def __init__(self, uid, filenames, directories, contrasts, mask_filename='', mask_directory=''): + self.contrasts = contrasts + + self.uid = uid + assert self.uid, "UID empty" + assert self.uid.count(' ') == 0, f'UID "{self.uid}" contains whitespace' + + assert len(directories) == len(filenames) + self.ims_filenames = filenames + self._im_directories = directories + + self.msk_filename = mask_filename + self._msk_directory = mask_directory + + n_contrasts = len(contrasts) + + 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 + + self._local_ims = [] + self._local_msk = None + + def __repr__(self, *args, **kwargs): + text = '\nInput [' + for key in sorted([k for k in self.__dict__ if not k.startswith('_')]): + text += f'\n\t{key}: {self.__dict__[key]}' + text += '\n]' + return text + + def info(self): + message = [f'input: {self.uid}'] + if self.aggregation_weight: + message += [f'agg weight: {self.aggregation_weight}'] + for csuff, fname in zip(self.contrasts, self.ims_filenames): + message += [f'{(csuff + ": ") if csuff else ""}: "{fname}"'] + if self.msk_filename: + message += [f'mask: {self.msk_filename}'] + return ', '.join(message) + + def cache_local(self): + from mrtrix3 import run # pylint: disable=no-name-in-module, import-outside-toplevel + contrasts = self.contrasts + for cid, csuff in enumerate(contrasts): + 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: + 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') + + def get_ims_path(self, quoted=True): + """ return path to input images """ + def abspath(arg, *args): # pylint: disable=unused-variable + return os.path.abspath(os.path.join(arg, *args)) + if self._local_ims: + return self._local_ims + 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 """ + if self._local_msk: + return self._local_msk + 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) diff --git a/python/mrtrix3/commands/population_template/usage.py b/python/mrtrix3/commands/population_template/usage.py new file mode 100644 index 0000000000..a3bd98093c --- /dev/null +++ b/python/mrtrix3/commands/population_template/usage.py @@ -0,0 +1,288 @@ +# 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/. + +from mrtrix3 import app #pylint: disable=no-name-in-module + +from . import AGGREGATION_MODES, \ + DEFAULT_AFFINE_LMAX, \ + DEFAULT_AFFINE_SCALES, \ + DEFAULT_NL_DISP_SMOOTH, \ + DEFAULT_NL_GRAD_STEP, \ + DEFAULT_NL_LMAX, \ + DEFAULT_NL_NITER, \ + DEFAULT_NL_SCALES, \ + DEFAULT_NL_UPDATE_SMOOTH, \ + DEFAULT_RIGID_LMAX, \ + DEFAULT_RIGID_SCALES, \ + INITIAL_ALIGNMENT, \ + LEAVE_ONE_OUT, \ + LINEAR_ESTIMATORS, \ + REGISTRATION_MODES + + +class SequenceDirectoryOut(app.Parser.CustomTypeBase): + def __call__(self, input_value): + return [app.Parser.make_userpath_object(app.Parser._UserDirOutPathExtras, item) # pylint: disable=protected-access \ + for item in input_value.split(',')] + @staticmethod + def _legacytypestring(): + return 'SEQDIROUT' + @staticmethod + def _metavar(): + return 'directory_list' + + + +def usage(cmdline): #pylint: disable=unused-variable + 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_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 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).') + 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', + 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, + 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' + 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') + 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 combinations 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') + options.add_argument('-transformed_dir', + 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,' + ' 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' + ' 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', + 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' + ' 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', + 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/mrtrix3/commands/population_template/utils.py b/python/mrtrix3/commands/population_template/utils.py new file mode 100644 index 0000000000..d642c3997c --- /dev/null +++ b/python/mrtrix3/commands/population_template/utils.py @@ -0,0 +1,297 @@ +# 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/. + +import csv, math, os, re, shutil, sys +from mrtrix3 import MRtrixError +from mrtrix3 import app, image, path, run, utils +from . import IMAGEEXT +from .input import Input + + +#def abspath(arg, *args): # pylint: disable=unused-variable +# return os.path.abspath(os.path.join(arg, *args)) + + + +def copy(src, dst, follow_symlinks=True): # pylint: disable=unused-variable + """Copy data but do not set mode bits. Return the file's destination. + + mimics shutil.copy but without setting mode bits as shutil.copymode can fail on exotic mounts + (observed on cifs with file_mode=0777). + """ + if os.path.isdir(dst): + dst = os.path.join(dst, os.path.basename(src)) + if sys.version_info[0] > 2: + shutil.copyfile(src, dst, follow_symlinks=follow_symlinks) # pylint: disable=unexpected-keyword-arg + else: + shutil.copyfile(src, dst) + return dst + + + +def check_linear_transformation(transformation, cmd, max_scaling=0.5, max_shear=0.2, max_rot=None, pause_on_warn=True): # pylint: disable=unused-variable + if max_rot is None: + max_rot = 2 * math.pi + + good = True + 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(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(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(f'large shear ({shear}) in {transformation}') + good = False + rot_angle = float(data['angle_axis'][0]) + if abs(rot_angle) > max_rot: + app.warn(f'large rotation ({rot_angle}) in {transformation}') + good = False + + if not good: + newcmd = [] + what = '' + init_rotation_found = False + skip = 0 + for element in cmd.split(): + if skip: + skip -= 1 + continue + if '_init_rotation' in element: + init_rotation_found = True + if '_init_matrix' in element: + skip = 1 + continue + if 'affine_scale' in element: + assert what != 'rigid' + what = 'affine' + elif 'rigid_scale' in element: + assert what != 'affine' + what = 'rigid' + newcmd.append(element) + newcmd = ' '.join(newcmd) + if not init_rotation_found: + app.console('replacing the transformation obtained with:') + app.console(cmd) + if what: + 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{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 + + + +def aggregate(inputs, output, contrast_idx, mode, force=True): # pylint: disable=unused-variable + images = [inp.ims_transformed[contrast_idx] for inp in inputs] + if mode == 'mean': + run.command(['mrmath', images, 'mean', '-keep_unary_axes', output], force=force) + elif mode == 'median': + run.command(['mrmath', images, 'median', '-keep_unary_axes', output], force=force) + elif mode == 'weighted_mean': + weights = [inp.aggregation_weight for inp in inputs] + assert not any(w is None for w in weights), weights + wsum = sum(float(w) for w in weights) + cmd = ['mrcalc'] + if wsum <= 0: + raise MRtrixError('the sum of aggregetion weights has to be positive') + for weight, imagepath in zip(weights, images): + if float(weight) != 0: + cmd += [imagepath, weight, '-mult'] + (['-add'] if len(cmd) > 1 else []) + cmd += [f'{wsum:.16f}', '-div', output] + run.command(cmd, force=force) + else: + raise MRtrixError(f'aggregation mode {mode} not understood') + + + +def inplace_nan_mask(images, masks): # pylint: disable=unused-variable + assert len(images) == len(masks), (len(images), len(masks)) + for imagepath, maskpath in zip(images, masks): + target_dir = os.path.split(imagepath)[0] + masked = os.path.join(target_dir, f'__{os.path.split(image)[1]}') + run.command(f'mrcalc {maskpath} {imagepath} nan -if {masked}', force=True) + run.function(shutil.move, masked, imagepath) + + + +def calculate_isfinite(inputs, contrasts): # pylint: disable=unused-variable + agg_weights = [float(inp.aggregation_weight) for inp in inputs if inp.aggregation_weight is not None] + for cid in range(contrasts.n_contrasts): + for inp in inputs: + if contrasts.n_volumes[cid] > 0: + cmd = f'mrconvert {inp.ims_transformed[cid]} -coord 3 0 - | ' \ + f'mrcalc - -finite' + else: + cmd = f'mrcalc {inp.ims_transformed[cid]} -finite' + if inp.aggregation_weight: + 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(f'isfinite{contrasts.suff[cid]}'), 'sum'] + if agg_weights: + 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) + + + +def get_common_postfix(file_list): # pylint: disable=unused-variable + return os.path.commonprefix([i[::-1] for i in file_list])[::-1] + + + +def get_common_prefix(file_list): # pylint: disable=unused-variable + return os.path.commonprefix(file_list) + + + +def parse_input_files(in_files, mask_files, contrasts, f_agg_weight=None, whitespace_repl='_'): # pylint: disable=unused-variable + """ + matches input images across contrasts and pair them with masks. + extracts unique identifiers from mask and image filenames by stripping common pre and postfix (per contrast and for masks) + unique identifiers contain ASCII letters, numbers and '_' but no whitespace which is replaced by whitespace_repl + + in_files: list of lists + the inner list holds filenames specific to a contrast + + mask_files: + can be empty + + returns list of Input + + checks: 3d_nonunity + TODO check if no common grid & trafo across contrasts (only relevant for robust init?) + + """ + contrasts = contrasts.suff + inputs = [] + def paths_to_file_uids(paths, prefix, postfix): + """ strip pre and postfix from filename, replace whitespace characters """ + uid_path = {} + uids = [] + for filepath in paths: + uid = re.sub(re.escape(postfix)+'$', '', re.sub('^'+re.escape(prefix), '', os.path.split(filepath)[1])) + uid = re.sub(r'\s+', whitespace_repl, uid) + if not uid: + raise MRtrixError(f'No uniquely identifiable part of filename "{path}" ' + 'after prefix and postfix substitution ' + 'with prefix "{prefix}" and postfix "{postfix}"') + app.debug(f'UID mapping: "{filepath}" --> "{uid}"') + if uid in uid_path: + raise MRtrixError(f'unique file identifier is not unique: ' + f'"{uid}" mapped to "{filepath}" and "{uid_path[uid]}"') + uid_path[uid] = filepath + uids.append(uid) + return uids + + # mask uids + mask_uids = [] + if mask_files: + mask_common_postfix = get_common_postfix(mask_files) + if not mask_common_postfix: + raise MRtrixError('mask filenames do not have a common postfix') + 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(f'mask uids: {mask_uids}') + + # images uids + common_postfix = [get_common_postfix(files) for files in in_files] + common_prefix = [get_common_prefix(files) for files in in_files] + # xcontrast_xsubject_pre_postfix: prefix and postfix of the common part across contrasts and subjects, + # without image extensions and leading or trailing '_' or '-' + 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(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%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(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 = [app.ARGS.input_dir[0]] + if len(contrasts) > 1: + for cid in range(1, len(contrasts)): + 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(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: + 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=app.ARGS.mask_dir)) + else: + inputs.append(Input(uid, fnames, dirs, contrasts)) + + # parse aggregation weights and match to inputs + if f_agg_weight: + try: + with open(f_agg_weight, 'r', encoding='utf-8') as fweights: + agg_weights = dict((row[0].lstrip().rstrip(), row[1]) for row in csv.reader(fweights, delimiter=',', quotechar='#')) + except UnicodeDecodeError: + with open(f_agg_weight, 'r', encoding='utf-8') as fweights: + reader = csv.reader(fweights.read().decode('utf-8', errors='replace'), delimiter=',', quotechar='#') + agg_weights = dict((row[0].lstrip().rstrip(), row[1]) for row in reader) + pref = '^' + re.escape(get_common_prefix(list(agg_weights.keys()))) + suff = re.escape(get_common_postfix(list(agg_weights.keys()))) + '$' + for key in agg_weights.keys(): + agg_weights[re.sub(suff, '', re.sub(pref, '', key))] = agg_weights.pop(key).strip() + + for inp in inputs: + if inp.uid not in agg_weights: + raise MRtrixError(f'aggregation weight not found for {inp.uid}') + inp.aggregation_weight = agg_weights[inp.uid] + 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(f'Sum of aggregation weights is not positive: {weights}') + if any(w < 0 for w in weights): + app.warn(f'Negative aggregation weights: {weights}') + + return inputs, xcontrast_xsubject_pre_postfix From 6a65e81041a6b13f1ea92666cdd3725327fbdebf Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Sun, 9 Jun 2024 21:13:03 +1000 Subject: [PATCH 100/182] git blame: Ignore splitting of population_template code --- .git-blame-ignore-revs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f2d7fd4588..b4c7ce0d90 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -119,3 +119,8 @@ cd8a6f1c2d0debcf3a6de185f8b92fc0be3e1401 #Author: MRtrixBot #Date: Fri, 5 Apr 2024 14:06:00 +0100 # Replace include guards with #pragma once + +9b3de4d4defc3c7572bcd3ab9f214b72ce91abf3 +#Author: MRtrixBot +#Date: Sun Jun 9 20:33:33 2024 +1000 +# population_template: Split source code between files From 9ff374bf37179713166037ed9b9fe81872bcfb9a Mon Sep 17 00:00:00 2001 From: Daljit Date: Thu, 13 Jun 2024 11:39:52 +0100 Subject: [PATCH 101/182] Avoid using CMAKE_SOURCE_DIR CMAKE_SOURCE_DIR refers to the top level source tree. Hence, if mrtrix3 is built as a subproject of another project (e.g. using add_subdirectory), things won't work as expected. --- cmake/GenPythonCommandsLists.cmake | 4 ++-- python/mrtrix3/commands/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/GenPythonCommandsLists.cmake b/cmake/GenPythonCommandsLists.cmake index ebcef2728d..93f3dcb1df 100644 --- a/cmake/GenPythonCommandsLists.cmake +++ b/cmake/GenPythonCommandsLists.cmake @@ -1,11 +1,11 @@ file( GLOB CPP_COMMAND_FILES - ${CMAKE_SOURCE_DIR}/cmd/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cmd/*.cpp ) file( GLOB PYTHON_ROOT_ENTRIES - ${CMAKE_SOURCE_DIR}/python/mrtrix3/commands/* + ${CMAKE_CURRENT_SOURCE_DIR}/python/mrtrix3/commands/* ) set(MRTRIX_CPP_COMMAND_LIST "") diff --git a/python/mrtrix3/commands/CMakeLists.txt b/python/mrtrix3/commands/CMakeLists.txt index 8861f055f1..bbfcfcd0bb 100644 --- a/python/mrtrix3/commands/CMakeLists.txt +++ b/python/mrtrix3/commands/CMakeLists.txt @@ -76,7 +76,7 @@ foreach(CMDNAME ${PYTHON_COMMAND_LIST}) add_custom_command( TARGET MakePythonExecutables WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMAND ${CMAKE_COMMAND} -DCMDNAME=${CMDNAME} -DBUILDDIR=${PROJECT_BINARY_DIR} -P ${CMAKE_SOURCE_DIR}/cmake/MakePythonExecutable.cmake + COMMAND ${CMAKE_COMMAND} -DCMDNAME=${CMDNAME} -DBUILDDIR=${PROJECT_BINARY_DIR} -P ${PROJECT_SOURCE_DIR}/cmake/MakePythonExecutable.cmake ) list(APPEND PYTHON_BIN_FILES ${PROJECT_BINARY_DIR}/bin/${CMDNAME}) endforeach() From 398cc04fe0d6ad789e0036fdd38b41fd87d311e0 Mon Sep 17 00:00:00 2001 From: J-Donald Tournier Date: Fri, 14 Jun 2024 14:29:00 +0100 Subject: [PATCH 102/182] CMakeList.txt: fix very minor typo in warning message --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ed4d875479..de5ede25e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ project(mrtrix3 LANGUAGES CXX VERSION 3.0.4) if(NOT CMAKE_GENERATOR STREQUAL "Ninja") message(WARNING "It is recommended to use the Ninja generator to build MRtrix3. " - "To use it, run cmake with -G Ninja or set the CMAKE_GENERATOR" + "To use it, run cmake with -G Ninja or set the CMAKE_GENERATOR " "environment variable to Ninja.") endif() From 4e2bc42419c8ca7b63a1cf29058bb049f5c66846 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Wed, 19 Jun 2024 12:23:22 +0200 Subject: [PATCH 103/182] Remove app bundle related files and logic now that cmake takes care if this --- .../macos/MRView.app/Contents/Info.plist | 156 ------------------ .../MRView.app/Contents/Resources/mrview.icns | Bin 232623 -> 0 bytes .../Contents/Resources/mrview_doc.icns | Bin 177431 -> 0 bytes packaging/macos/MRView.app/Contents/lib | 1 - packaging/macos/MRView.app/Contents/plugins | 1 - .../macos/SHView.app/Contents/Info.plist | 20 --- packaging/macos/SHView.app/Contents/Resources | 1 - packaging/macos/SHView.app/Contents/lib | 1 - packaging/macos/SHView.app/Contents/plugins | 1 - packaging/macos/build | 12 -- packaging/macos/mrview | 8 - packaging/macos/shview | 8 - 12 files changed, 209 deletions(-) delete mode 100755 packaging/macos/MRView.app/Contents/Info.plist delete mode 100644 packaging/macos/MRView.app/Contents/Resources/mrview.icns delete mode 100644 packaging/macos/MRView.app/Contents/Resources/mrview_doc.icns delete mode 120000 packaging/macos/MRView.app/Contents/lib delete mode 120000 packaging/macos/MRView.app/Contents/plugins delete mode 100755 packaging/macos/SHView.app/Contents/Info.plist delete mode 120000 packaging/macos/SHView.app/Contents/Resources delete mode 120000 packaging/macos/SHView.app/Contents/lib delete mode 120000 packaging/macos/SHView.app/Contents/plugins delete mode 100755 packaging/macos/mrview delete mode 100755 packaging/macos/shview diff --git a/packaging/macos/MRView.app/Contents/Info.plist b/packaging/macos/MRView.app/Contents/Info.plist deleted file mode 100755 index 56b853a703..0000000000 --- a/packaging/macos/MRView.app/Contents/Info.plist +++ /dev/null @@ -1,156 +0,0 @@ - - - - - CFBundleInfoDictionaryVersion 6.0 - CFBundleDevelopmentRegion en-UK - CFBundleName MRView - CFBundleDisplayName MRView - CFBundleExecutable mrview - CFBundleIconFile mrview.icns - CFBundleIdentifier org.mrtrix.mrview - CFBundlePackageType APPL - CFBundleShortVersionString 3.0 - CFBundleVersion 3.0 - NSHumanReadableCopyright Copyright (c) 2008-2023 the MRtrix3 contributors - LSMinimumSystemVersion 10.14.0 - LSBackgroundOnly 0 - NSHighResolutionCapable - - CFBundleURLTypes - - - CFBundleTypeRole - Viewer - CFBundleURLName - MRView file - CFBundleURLSchemes - - mrview - - - - - CFBundleDocumentTypes - - - - CFBundleTypeExtensions - - gz - - CFBundleTypeName - MRtrix of NIfTI image (compressed) - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - mif - - CFBundleTypeName - MRtrix image - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - mih - - CFBundleTypeName - MRtrix image header - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - mif.gz - - CFBundleTypeName - MRtrix image (compressed) - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - nii - - CFBundleTypeName - NIfTI image - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - nii.gz - - CFBundleTypeName - NIfTI image (compressed) - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - mgh - - CFBundleTypeName - MGH image - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - mgz - - CFBundleTypeName - MGH image (compressed) - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - CFBundleTypeExtensions - - img - - CFBundleTypeName - Analyze image - CFBundleTypeRole - Viewer - CFBundleTypeIconFile - mrview_doc.icns - - - - - diff --git a/packaging/macos/MRView.app/Contents/Resources/mrview.icns b/packaging/macos/MRView.app/Contents/Resources/mrview.icns deleted file mode 100644 index 542cf14c271ad43fd1aa3620eefbefe32727ac8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232623 zcmeFacTki`)IK_Zh=PIvR1m~0ih_!gqvV_!!Z0($8DMhGIY)vS1=H&8s$c{~1Q9W; z0nk-cB&&dkfFN18?|xOc?(eUAzkhEPK-Ii8p}XJibDrnvb2vRY85kl+PgR~na@Y?7 zMV$u$KlA^KUp~lW-PUz$4D~^v6~yfbiTNIfJ=V*Y>9>G(EZ=m%AC9Lp zkm0GhEDi@3?2l*h7#Of`98-?8cXoGlba4*y+iCI|v|DezOKdg{gD0XhV|<*w5o{_6 z7afC_hy=tW-v|NKH#9cH#mmJl#NXCfw-IE&=3pv;OrcR3d{SyADHj}^g{KHvsgYQT zl#fP4#AN$KVVP_?EXv*9Kg?&h$#amy;vHUrkx@aRQ8BRW?3DPJ(1^HD6bT1QgT>Ru zEOch7r<1E&WC|3T#lVDn=JH9Aw#H9EPK$T>MJ8uwXCiYmAt@;^XmT=&5E&O89g8J_ zNqI>TQQ;ALcEk}dv34HGG(t!uCN4VC)qIKGBan;Uu8>H0dIp}s<@4C&Tr!=4=P>+z zQCa@s;en8FuV?}Z=IhQBF)2x2uHiZ9PVTYE!LE)w&6n1JJT2`u#)c(f^J0^kTt1hN z$Yt}82sX+SA!H@G+u1s#qjLnDG-z-ZJcGq1p_4)bJiHQ!M2L&6%a)bKOYehxcc(@> z`g%vEq{jQ@azuP|0)@{_%4LEfLaBs}i1qRarSdt%xUf9|31QJ}F%_Si8RP98lqbjy z_1I@?XS?2H$>KYpfHhDE7)QZjP+3X7S!@n2ESt-rga-z=qC`@;Ou|7x!jd==J}NFD zhl~wQ+V@3OGzYEiH!x zMd5NF;r{ViWIh!d3U*q485FB$<(vUa$#V^jOQP^u>7lOhTpX2?>k-dk5!g~-cvP5Q zbT&gQQ%U(WWD=anmkE&B%uEzlBG)RpBx)8?%%v04A_M%BFjRO*gv-W7e}du{?>G>Y zjfzQ$Ka@`6GqK^ZF$sxD;Dnr5oQR8sBiVAfh>A{6ia`Ol4BRw15h{`isi}x4pO`GF zN+A(2Q1MtcPsG4u)59ZEQ5YyJZnwcDP`q`#i%%?#795FX2)IZ{8k<3*)2O-OF-)mQ z#KvMdN->kpqG!j!nL@P^SpP(ZRKiP3KxAelM<9iAseq17j3&!?3<@d-rnFyH^(FfJ1GZPAAXn% zQ6`shWeOQ;U$jIe70Q)LNfv}I0ZgBcij85&q+B#YDwlJT$!NgC5aAS&Ou(iyqSKT7 z{GrG!RCWI1QY(ZET7+LRg2>|H(3B+SLy1x0ZmZ9MQca^1 za%eoML;%6CSzNiA03%Cfv}~YFepV=iO-kgdl+4&1rBbGp^Z3l1c#=X$h7uJjwU{B6 z3VAY8G)Vy1BR&8|<_XvUHYCREKM)21AKbKimvIRQw%8RJgQEgQz)dDfcpRykf@Db) zdYMDgM2+vc9nF5(iE`%g7Bua$Mfi~!F-KWC{l`OSqQdRCg;WBCrzqWDS`gDxlt$~Ks~%{s#w6|(MgG+3<*2S zH6%9hpmRFbXW4O(@$!8zz6hQU!&2D-p-RnD#mFTRcDz4Mrj&+w$_mt48O}LCT&M)b zEoEoN;3R4VCyglAs>LKo9`GiKTq$BgA%GQ2#cVbp2Ka0-BQ{Ga%sG^u2ZKe2;K6H- zgR(XUX9+c=AUK1=;Yk!qtwyU+Ym@>iOCV7|e5FT+q@-ZImmURWFFKHbpk-Po1Gny%M;Sm!Mm3q0U=ESB2n=4JS2(E%uP%2cXYFJ4UY<8 zDKz;7s!V&5oJ)^{! zz%hm-5_8EpDH&oZEB#P3j4I@(*;yJK2IZLhxFz9*l3Ws3$YYR@sqh>cEhU_#)D#_- z$2tm>Vsuy}L#a{7{rDkc06tECek#^A1g;2{=Mw77peB zTxJS3>!3~{D91WF0n3w$coawkS|F7RX^MPxax`C+ugHU-vT_(~iAF%oq=^+Om4XjD zv?qeA5K9$u8DK#QJ~xM|(#YjP3Os=-q6vXl2oSPZOiRWPxe}=)JuV?Ehs8-v6>}Nc zHie)(eOF`}ks<;h4HM!NPi8`+C0b>A98;*k`DcoxJPMN|=JRqfY+z{=m^j3~4RFBI zG%~47Dp!a^R6)KH@NcLWQo#qHM6oB&9 zrQk5k90u@N9z8WYm7I;ysO6|Mo}3dO%>*Ej&lB?LSTIrsxRVf%WTZN{W-wV&u}~~g zDy1^EEMF-Spo7C0QU>4-iVSxm0F4BHEYKMtVSf-)A`($z4tc~97$N#Vce_w%8WAmI z34~IWQp^>xMH-a|hZHEWV1if%7^74ylMtau0Gz~p4w=cNMBBRuX3~Vfgev4*rb;Vk z5p!`kzJx^-Dun^Te2Gk!2NB8yXapcA#3F%A%)&>w!8ox-`JkO^0|^*L9#_cb@uW(n zTBVXm6+#vUDHP*y0*OSa0niH=fRM!#s65mFU{ZZOciTFLVS&*}7;LqMn}L-|0A^=V zMAAG@q)MUUf^l*Q5zFWD#DI7c@cE*&fJ0cgMLuY`O(Kax!}9R}WdVcbtCQuxG;z|% zLV%e#T(v@?RBM$AIXjD|&DSVZYCxMnzRE zaslDbr{f7UzLW=rXSijPY=90;Lvy%zvPdq?4NpY~cwAt%B^rP(C3w0-DWq})60Kh48EEX>6&5DGZFSR~BM3a~}z19Kn;=7o~SRZ97& zOh9#tMZ_Fbpbsk16zFh$e5Ra*;>lGxC~_i(F9PNd01%}{#)W~Yd;tSZ;>y(d>Vg6p zi^O1HnEC&$rd$e8gy=sKOOK5TIJhq$lg<@M@X1j3D5*xRmI&lRd}1!Zx+o}1ESCs) zR5CiiJARLj7KB<6k}ekH;id8%o|Mc}YP3LWfU1)#q_m(2h5$#A<`)6TUvOBf1wa## z0o)qk^AfpQBjPHxDuB{tH2>|+vFRvmTw-)EUc{zi&^Q7i2gMcx8iFVTG&F^R%}sIj z*t@I%w8}P?PGh4vf*i40qR;@=FOl&SOVtViEhm~HmTP&1fm*pzTU4NtvT=BvSOy4A z4Zvnvxjb2gr9?nOL*pnyu3Ve1lJhk7 zB8FTJaF9l&R8ZkA)^G z5R=F}o=_ifv;vKar(VR(<>3YVLbD<{U^6N?2Tc+xHLP@okRsM<3;6l=)QIFP0`EU? z5pg6WFFPo~!zK#Nmx>fZ5sxPjvxs~-IVLhFA{7SncTKP_0_7}s^ubDLOc^PiuLgWv zsbJ)=6jCXJ&L=5VQjtU{AagnN>|~5cRe1QYQmqk7WePw>sKm+s{-FpWVC4X}i}?g! z8;2cE;9RaqEa9^mJb_FqVrNET#TW=A6&#m|v;#U@;d3C8&XUNPsT385%L3Len)07j zD!5dUL_o|Vk&?W^(@=Cdus-?u+M*(*oJ(N|_%wV{G%O{9&0%u6cnX)v#zkzl0cT~Q zX%c}@A`}2DP5?)eN6ikZSYu6-*Xi zB$WuUQQmNZIA6=pcP}`61aJklfWzl=S#&0qn3>CGPyxk><5F?y?witCDKQ9vM8ua1 zNbtDe1QMG@A_MUeDjB?4_b_PJ4(Hu2DO`mR;t9diWd%j~5+MaE)~fj#D2evSVSuAi z0Da~c0#Zewb<8h1d^leXxSK-21^F=fPpz&40{3Y7_ z!o!6HM+)+J1sgPMx`0JzaDWI&Qd$~`g@*cX-s~LZW# z$3d<;!JgJ@ZS404q(cH>WDJpz5)ll`O(l!ffcqAx=`?jwff|4lg;K3hX@R>b%+D_X z{sbgGPqu~6Vqh}SIXFCng3I(>Z@7NDS4d<^4xT_j!Q$gX9d?*6TT~3%XdV;0f6a2k zrJKT`p}`p;UOpc7HlAK?NIB3@Q9hq3=M^|>I79-KE#flxVgQT*9jjCc(=r4Kxj-Ti z00Sa1SzL*Tg>YYPv}{ibGS)93)MLlG6~>Ftg5vGKep{C=T4K80&1t8z!`^Kh%s1|E zJd`cdXo?Eed;x$&8YzLulc}^?5f2C}kbzL4h=++!7qVF#nL{wxV2^3-E9`9_r#=^>S%icYkwmCX^?Q{0tW#dm`3N(OB{~vmi%K+HX{wEHl zVj>=$ltCgh(F_TTFJuA11s*&;4JqXiX#yd_$(RSyS!VDzD13#Jm-Qxl&+u4qI1mTh z7LuNp;<0C6NGzPM&R6jP&72RIt~OuBA!4~)p+GKSQdn#uU#gT#ISh$PtKq_s% zI*-kSZM_BxUt+mx)qx;Di6zIUIPdmL$i$>4CI)&2=884>YJgr8JPIO%&PGRsk%a)i zE0sb-7DcR-0tl!nEC8MmB!d|MOLOQ<@MfKBpin*CWp@7YSYmQyT5MF%p;R0(F(Ax) zmtz7Ja56p{6X|`>Jth+#nL^bRsDRCv07v2h2&edO>;Q9UWteD!9B@@831W2<6uiA%xS+%2skDw4Vj%5?(X3c7!nCa3*;)m*c4(4SF9-jNC+Sn ziAb&D#byhId@4CNW;M{5!3K|XGM6q8h*eSn6`sjtu?eu~XmET)SX7|LArD8lkl1i< z7ta{HBEK+SqXAqy7r+g*rZ8VA6a!lxm_SHG;$yey-2??Lw)KE2A01`Cqypq`PoY4fkt{Y4QPiNut#PxG2KYfPE4gg(BlnNe2%2MBpgF6u<^F^dR3%0T2=63xHr4 z`A{5DAYhRZi6IvEK;9dGEh%3}$w`ev^TjfOSixnB=){~n3XOsTyQPMPa$&(tm0BxL zx3&%;N#qIzK$1d2aB^}w43dFN-gFCN-j{r9d9X@E~5=&<^U&JND6R|Q@PNMfFg9o6^tKu0#;Pd~r46Ge1 zFFhFwNrI*Zgr{QD(okVANu;h6v|P_7A~`lJEH&P1+d3nihoDV*4mrTC8_=iqSn0Xau%NKnz792G|mro_WYW92}oXOLRwS!3OOhD_y%xG7y$i zDF7)a7IAX&@X!z+?LQRGZJBbrznnD1lM}YykLJNNh-$zvEUPwptpf z_Z4Ke+{zW43WcPG?B3=Qo*wuZ&FqN=X*;37pbjz&_Z%+A-T1X+*(L( zEhM)Vl3NSOt%c;)LUL;%xwVkoT1aj!B)1llTMNmph2+*ka%&;EwUFFeNNz18w-%CH z3(2j8?# zjwsZrISX3k>=*94M+#aJkcmLRlR}(Kg&@N~BnC^OlJPllc6`vX2sD#MW>eWz0y<(l z541ap&Y+PQY#|dz%Jm0h*aQcED&gWCYLV|Dnuk`4l~!D2Qsn^PoOf` z0s+l`??JU^a_D1~D88{Ln@(YY9lt(IecX0<j!J}^iT=&$6dsbe&5}{6OR^{>a00gwrB1BZlY8x^RUeqNzbfZd=?yS zWt;j}n66`_5%qRThwnos?8Do(xzekZ0aj)k@z&`#zy4_?)9MUgihoYgVN4fTdwomZ zFrMC5xzGENJ>yPSMoT@>lcUbb{=B>UjZ8m2Hdl6Rh4w|mWWchSeb?T>&V=U<{N4BE zTzWCN?aX!DP}*E6x(+EGMb}?yJW=W?8cKuA*dE;0+ZFvN^zXvbfY~M8d1!WpWoPTr z;)0c4i}OFVpDe$)a>x&IrPbi?n$inbZ!0!&dPR<-ySw+u8l8IoiLHBo`R}5JQ+1R_ zl?Ri5{&ZXPKwgz{a^wkN1|3`Sv-iulgD0Oe>3*}*)%2ioHlfsIu%Fd8DmuEyVD^t= zhlOU3gfi~g0H5*R?z!uqLRRRR=URLT=GqmWH%_tdNkw1mYYbhs*6CmC&@}_o{FpQI zem9i_UU|~u%h4)ziF7dfzZSQEz3YjrZb? zHSdvs35^%;H@#|Vu7I7*Uc0{cf%{L_TPI%D)jT{r_w*@DICbaMtzDT?Pr;fS7PFcw zYu;pyToywzR(o-FJL!6DH{ZDGgRl|0KX|+Vzh7rFUEv-+USxS=b>`6Y`w)~r)ZOXb z?`oAp%*A5=HkVuX&cNp#c7;J=TdwRju8?ebzH>%D_5SQ**mX$%Zl~yqZu8Ss;8#Q87dpA0!6TJ~C&@4F-3Y7yj!Gm_<1NsOd)5p$pJhbx*?D83$ZBrazrKm0%vB_Pa7B9FyMKeSZhr=J(_KFWLXFU$S4Z>r!x%l|Fe%#@mRteZ}=Ng~Ys;$16s>6(<-? z9kt}~pKQUbYy8KcUEg_B^zlY(omD{)%c%3;iSOgJ)B9IaA7t*Mk@h0a)Rjl^C2I9uC?CTrUCXNOM5IQ4jwECKkXW2&8*P z_y2xI2Xsd7|GEC}MZe9?L=eau6zt~{N7MPVLOS_IJP}{CU2fbt-OcmPohPxy^bAZa-Esdvy8yq{Yk|b) z7^ETSow*HNmk>K~ym=O*Gk05i<0Y#03g_;iL5ZuR$|5_Lz(@|SN(FN~6^ z?Y9nH?mrJcb>$c%rsl9EXIl;9EUxsbSBX}akL`-E9n~7KOa)2 zwKEtZ;GoMcd9(;vc4Lj=@rL2Iwjp0b4{m}PM$}k#oRW^BCd!Tuf|F@rMWe>{kGnCm zm%m&FUpX~eIA)eN<|0Yec)fb~!JxyyEp+xsXGueb*vqNp*bmf!KUqco4!`;(=C*00j4bxo?9 zME}dp46$c04FXkV;;L{-8Q-pnp1FETxy3H#CElk$JDLA z2d8&ecd8N1x4G*YM;ymCpqca)d6sseijro<&PTV1A0L3YdS zs*YrT@T69KyXo(qhqaz1q&<7zT|PkG!0D;E|6JTqe@C0$=Jmp91I_W-?X;0XSP7zs zf=w+a29ynaRgAxV)8YPG)8A1%H`woWYVLf0hX-^X{@6OQ#guFy5H<9C?VkvHGH|rN zqjavJzvH;3vF1^PLB{Bbk`Ln}ylOGFW#6ZV_P5)A*2K*2`JKdAg#Gl-QqH;wS|?!@ z!sNqqT~Z0U^*;IjEAx+4E5n+TYkz+&#?B484ONd~Q>kra>hDeiugSSc&d;JD2=V1X zuak3Q4vp@kx2@-frKQu&TF%%pJuYF`%jvh#8>>~Te!c6*-XD3qo;!6k1w(I9nJh0& zF_?Gjm}#Gw``Le~bgt*@%xE2c1%#^7zN-6qwdcpRrqUCb$V;u$3{Y!J#c=4=9{h&( zs>)Z*Wdk?A9^2Bh`&6g;vI;K6k32Z}uhC?dm&fz6JuF~?jFD|NCrsy-zZp7N2ImgE zE$95|pbys>44o8qJ~SOHi_~cxkNfPQq|NJ=bk%*l}9# z^V@P}+rSUzWS7+b6aINRM4UO)M(-&l0pWu0ZNYu2FCjn=uEiamdl?_)VT*O_;rp2TyQwsT1{ zw$)Q1*57+6kk!Ax{1DwleR!DSluaE7ke};t`F)@0#?8b`!Kwx;S~nDrl(Px zY2KIV8rS4}Kt&U_&i|m+4&>h*K0UK*1^#;&uj`SmWvYrpt+ z#oa+H{q1EaIqjVrEdDTtLTq3R-JgKcI{5~6FD7*F=_XVgQWQ8U=)o_%5BQd`$euHz zyjR@OH6n4UPJ7ak1~P~o>r)}CSD8*%Z?yYFZb`jYzqm|KPqS7Q?K`2yvU(o&dd*Lj zvVIUtXa2D?Hk?kWpO>7irK0X)jZeE}vwwHak)|DpL9_7hQ?FR2bOLl2*L}1`I(%!S z`QgMmsA&ze?|eft?rZBWUIctFwg1k~y)E6>PY*dd%>V}Y2dv9$Em1y4z~pTzr(78h zeYuVIBeY`XqL-up#Jj(w)X3=j2t|GGm>X>LWDA5gmC5G%}3yjp`>pH*_{d-NE3cyod4c6%43_n}q>yJv2{~jhVEn88RZxHq(4aHoO9L zFB|Z=!Rn|V^DhvdM6cs5hO<*Y(gKA}slB|B=+2{O@Al@wW2(>#@^3Jg)`h`Kb~B2Go7UWB8!i35i=JOE17){Dz;ecnQZ>n*GSvyFpaC2>8yLW zF92Huz^z#;w9Mdv#jS(RwzDzBx_7C)CgI#h)1%m)4u{AA=_ThMB~!mjK2)`SxLU?D zyVAbtjQ7mC&M6P^%~s*%_oi3UiM}Zvr4qYWIZ zo&93+D)jStV*&p+)^|S8xBHUW0Cll>art#L#`OH2h-;6>o6D}UuhDMTmVp@uZkuiR z9y8N*KRuwb{rT0aKFMTQ7vg1?QS}e`e0%V>d8DGhH0<2`<4;YsKc`!2=a!pz%?@83 zHLay!UKXv7S!uh~|Bsykqwf^W4zLm1O#S}s1XA0pJ~Io5_~OCJOI>H5TCTI&)%Pp2 zU~n;&=j=ZGu;nuEMUCi!!zoh3F(_>(X8Dz425s!>rq^azmSvFV9IatpZ+=w{i3J~S zcX6#ILiMOBdL*B(us>Q?(X+Cl@wKRB?&y#qR+DX=I#Vti`9cAGH+t$A z)rIO(YLqIZy+m#+4>VzJiR;qfjWy7ixodlNTcwGdC;D?TZwYaTY9 ztUq>jA~Kx|Uo}%d>N2G_dUL`l{Cda!p0XvTqRp}Jwp*CqLaQ*arD52pSkeDffNgb5>Tu`1V(-&y1Z;*&zj2zqfzr8h(cPkr|wy0ozm*`GmAzn&)m;otpOM zN%(w!B$rV+v_YdpU;7A)Dy0nF+Qb_KO<6E2}J^uI)%Gkl|`$mi-c`RIeT# zz6MhD?d#{R%eId_Jft&rz|8nM`WDYQe~ z=_Y*?e*Sqz#!Lp0_xm5qHIeE$?g7@^1b^nsrN)SfqD98w znL*f1O#8<>CW~t-aNT!*+`sbG{+AZy!XSTJ^R8-T+1>71D&{Zl-lcN}#L+qCsF7Rw z>BjMvsp!+|VY;?)l~>k-sc(WYxYV;qGpPRMD6^=>oY_w@Mf8Zxk~WJy>uOxxt?Ze! zot%L_wS}u#X{;&hYkpgVfS9bB;k@YkN^!pS=J|tDspBursxaa9l!6TP;}YL<_B&=K zy#rlaN1LC8)<4<7HnxPF`8l7mxOZ(}Wu@xHJ;d0==2@uDlQQe&kG>P8&z$Z=_g-zZ zsB|H;yKb;Ge*f`RYsv`p#^|vdIy0n{w3)AUagD;FN1na&^}(;6v^6QtZ^x7U&#&8+ zpnrXhxrKgL!ojizB5Po<F-*n$|mfq*!kOq;8@F(pUWmq z72Si6Z+FyzI$qiq+&6AQ7N%TBVghf$ztTm!<@!k$^dJM! zC7$OpZReerbzYCX8EDKn4{jN*!E!eB`x0%c$;fsO&&gilbor&3k+a?X{<|dIODo2I z@e;dU8vGVv{xImM7_V#2`_k0>ZLESqA-e-X z9W{g$>~hR2{i`_vKK93@UW(YYOpyioEchL`HeO#XY<;g3(h>3eb1ITrTc-@pP z*2#0;Cfk625bB!xzRT>8*Z61OihL7mV|J4;hr}B&ijvliwYw)8 z+o$vMi{DZg&F#AVz~w8zwo-}#xtKiUBqdR)E^I2n)FzFv1JBF`jHl(a1d^c=$vOcWDUHM`J zxk`7hJL$C8;b`8>_{Yl1@@;t??V-BwiUt}Iw(lrs@bs~_YF{x|3GQdF8g%+P`fKQw zw!8hzpQZ)rQM0QOHVcBo&Tsc2({LpJrcH1N{>8zu;a#|~DmG!PZ#XJb5sA0iMRdyX zQa^b9WLf1ckJK*yub2mcb=_UW+ndSAz7ayJ?KpG=X%OxMoGd#(TyHXjd#ZoF)OS%t z&C?B(2mPS4#YbzK#!rLVzB6~``Bt`!LD(u6xY)E+I3w3erA)aUt!?@7JGqn8_M zY{pKY_f!uKtvGD-cseYSkSRW`-|_Q(cT%2hIk~2-b(1|8g+F$6Eq6MGren%`yWaK+ zeT)0sf#RD^Vcbl`AV2sdHS)&q=`C9KpMTn4*m|WnxXxyY{A#eL{LgD!Yg1lw#739W z_qLec+`72jd8GeZlr!#t)acasv$xG3WBooCUG`>edhc@<*{A!v0<`&axd z0vXu1J!sy=FHUf^XpS!z6pczhLB{A2VH^`eqpu1BPI z<-Z@_!-}$dm^4vb?{b;^y#DPwVNOT;%6I*x!1mH~X4gyTcfE~>vae;=zN%fOn}ct` zN_Px(lq_P8ZtZJ}JwSco^trOSKOFm!P5-+6$ZTQzrj7d!%27XDe%vhke0XeC^trq~ z)BNo&b!MWA$F~w2v}c7Wd)f6y`r>o5fAq)tmYS8E{e2I>BuS~sgYV6o)?3E(nK_*P*t_}`9Cr)4 z(s;=XY9a)_<^IT~BKt`~-;Z_cz4l5)_DgH`yK-_S+4%2MFJAm6XLEZW@XwG}Ecx_t z5TCqv;Ol_R)2qex9_4i{?@mE_Tkl)_9I-xToAC$Tr`P^#An#O##hr&Ioe0&u`PL%8 zc@*d3ytd=#(aP%H>rXZ?-)?<}E;{8j`3`a}cy}qxEOsgSFnram=zZVDNpe)jpWyd} z9mzMQue7Q!(R#GTCAvpuSEXw0T}E5*)T*8BQ<`*Y29=lpNBQiPk+-`rTP!KlFUpI2 z*H-tL*C6hu>k*Mnt;zh?(mfdB-aFs7ILznwfTHfunzj{py_XJMt1H47FJ*oDIa|QS z^>_NG{rkQC$uZ07z9o+6Ak3Y-P7~FO9oci^{QGF+kM(p#>oIX)?FBr3pD_)5>hwRB zH^#@GUmkyOyK}((Jq3cV7-bK@N3L_CTa{$ccj5zpbe;y2ep4?#ogcXS#cR5ZdjI** zkDFu;47{roPWLxEjbt{pVa){RFRf2z z>me)(-|m8n?L5h5BFPOYI`#bWv(ZfrkI(9xpij4VgN{WsBi!a@J#N+5-ocL1 z2j0)WN}E60AGxgLc70b}!9d1Y-?Q~Tub!SAokqcK0lMz?&*5r(TVBoUAM5sTuQ(e` zev5#8*MgGnxt^zQu{8fxL*G1K*Q52C>!McoSheijTX_ue=T-&~H=qXp;k@20^L ztUE*a@PhRQ!`K2FOO}~q>s7Ngvn0pY=Jy4}^#g5liob`fn_lZ-VG-T^Lo2Q|YL_1T z*(cs%)m4=CsPv_w2lIVSyUu6$z9&M+^3f4pB?imo(nwx##(c{F^rn)vgC8qN3DRl@5*nV`%9>)#Y1_>=O4ZX zpvBfcnz=xo65+@7w4CY)zG8^;!LlgA9 zv9}DjTDW(=MZ$b;E-%pog{=fba8|k{KQKYCD)R6jMx{3Uad&DmfMdGPmsIb5|BuP| zv-57u{*yIUds*K=FJWT_J&`Y!e&_W{a4*%Kx-LuJZ%i1g@8JJCVLdqvn;ourEPOe6 z-rS?y{8Y;1qq(trfsfY%qEY(QcS|p9`FHL*535D6k2}e6GDFOsrgyC)$1}mzGZ#L zn@m4Xe>%FcZOy8HZi~9DCqG9gR(Z&`-yObYQYng`#Dv$H_fodDE|(;%8Zgu`N!|RA zy7%mnF}PvkX4^IU`wwY;RXDh=vpj9-41glfp_lwJgW$DzuonegEQpo;6p__^{=34wnJ@01vbP$JwU)PCn zwWxb|{or9@NfB zP(C{RN?7jPI5V5O0hChTLLLo}7=BFIex}x|vL4$QPO04(x>0{8-}L-LGn)U!6}tk6 z@;`^S?nz#{HD~Ls=*>MD9m}`s)HT`YMsgz4S?;lHqP=;ut;HSWv&|iKpR;sf4|{e_ z_1jz@5ue{y`Xlej%^KfRUZyKgpf;A*`(1yytz$rB-1V?h>ts2->uAM`^f;?_}<;U$Mb#F ze05{_JDqC>QNV(~csdcTJKJ02Uc0YjtfTitdH&Y?MQ>O~k1jFRcPj5LQhmjI$L!GG z>~m0lF7)fF)b2ax+tzbd-4twYbVxoXD!iN?rRv(XPLA4B;4v|B;e0(v^$Fw68c8$q z+;)pDu85`0Ay1?9&)M2Bk*4Q;L%Rx!%0`|T*_|}p%^0Q?j~aMb*%*VWACOacJ@xd6*;gg*}_j%f}&eE%Qa)5Ck9szFo$28R%1-Ax}0E} zCVo+vbx_`4awu-HXfCUHpf#Qx7;ol>FT+}#d=gN)WBZOD>*i+~HaG6GcCha~xEHzB zjOAifgC0Cs(NVSX6Dze;Uys|~s-%N5Nn3heaOs8LO2eBx_j}pb{?XyFHt1PXQFLe9 zKbSR3H|d!DeF^v?o2>H=RQGf#xt+?wR&c;KyePtwto$Kl~e zKR@wyrPzwIT@1dIp=Slve6_fA51fUvhJwp1$LcFt}a+@7ZtuPipI?QeHG&Fk%;K zYhBF8`1JMPQS6mzKu^O}-JZM6- zYPo;ele@`;cj~UikvA1vEKf|<>};RU#q_|6IYo`1JiS7o{54_EHf=J!+jvCR_PwH9 z*Oa#O((xwBk_SbqzHNneQ=OqF3;!17hF6HlH-WDgRJ`k3PZXaEvh*H1x0*BQcTYpw za?B3(dMo*vA$1RlX?WNe^}3F<+|qOAeO`oQ{ii*K5trhv5A;1#9NXtiH`KgD-p`-R zXh268OtKEyxxMK3DV;?|>TNMto?>c~^fmK?)*3oH&~vJw@q>u#+2D1r=-Io^-Hk19 zD2cdY+4M`Tv9?khQC{49OgP|`lJKL9UUj;B;B@K}?jn zzm+a@a?NXlT)L?^1H{q<}9s4Q>jL6ZNu{A<*;B)fU^ zA4>Vk;Kb!7l%7RDy^iZnpI&-ro|1QLNnsu2*v5c{bA5|o=WLvP*5G``Gsj|e>nh4C zwbJdu!-c8@FHT}mMzWe^IH;>8c|LoRou)h(4Bi_F-*eMu_iuu!+ z2i?{x_#@EO{OV18?W7aJDQWWh5!GxEcCI@RN8Dq5qW*gC_yr^DE(3#_2>KSw4rAEd$D zo9i2jwSB8n88;d{=WnJWDg`}W!()nz2kEr0T-=!1Se0~*{q)%8Jg>HRNYh!>rl0V$ zD(Cda!c}2SJms-s01J zt+@z1(sMohP}Q}-OY85_jy{Dyk<((f;ywgQzdzd1`u*7F&an%=d-oT{o@?#3_k=r5 z&4xQxkwjpr)(PU7^82{Sv*zJvo?A)|5^0?u@?T93ufL=B_N-atwXsDAXOycHpE1{l36k`RlpMiv0|$mZ@tqCn~UB z?-kW16+d9(vs3P%ijdpwn!}@M;{{6}bN3t1?-(7Z#;$U&`$+MsK^K4Q#r2%6PvP5uf_uhyz`D=2 znLEuCM)1jJY&M1;D%WpvO}@guGR3bZ3pX@u?#y)S`$U9>d}H_pJI9=4+pL@JGX;Z^-}ITFh`^x*LFM zQXM_ymc2VZ8Zz!D7V^sEbPJ%D_y^e_9jt1>G=0FN!huslDme5J8BSeWMM`k)(kK7c z6?gk=Q#)e|h5UKld$u$eoAwqCb_1_i>v54aE)$&M1{YNVw-`#Huj?kr^<~_p;H}t_ zZ&_%$Ns|>=^%Y}pI1w^|ohzCvUy;PNW89A(>)TopR(y~ui*BJ=7tc~J%!R8yLCAw1 zr{KCcS7gFF$~5x9MT@9+Ik46gR>)xuZdduY3nNrRBZt&L%0tOSQRgf4w-;d%Hbq z(3UU9UB2tVSRVBE3RL2|rgUzUv1+SGP1xz}^^E|Re?RQ|5<06cRQ*jm;aIV(pqy+n zGbhYW*de-a)I->A-g~wl|7_wjZ(~2@pF*~~!v)Mt2RsjQph-yxWyMSA?%SCD?ScLF z(f;a(nk^6qjC{#TK{Kku@&6i~_#IMH=R1W9MwC8H2MPLRk4UL+p{3A9|v39QIW89gcu5DSc&h?(P=Q=xN0`Y`}1K zL6bYi;5wgJ&L6SqCB>nQ2!KC(i;*AV=nNhBvs~bF)R}U7X~zY zKrRr7rM1u$Si!ErR!*+M{(Q!fP*}t!UD0VEDI0T%(xTlRr6Ln9T838d})<9;AzAroa6j5}&I)q* zhnNh3tnuzq9@khXN%0vOIhms?^kqcEay+CK)|@JQ(Cl3U{YHkFapPUuCp40K_`?T8 zr*`5;qje&UP^Dt;S7VV_bHwS`|ME&5W%4d4tmvJ}?9%xl5i_HTtU-(LHyLU)rAYtQ zD0sJSJM*CEP$xrE|7*V=7;jFQ9&~C|iHh86U5RU18vk2`9S@kGpC$5g9^OQ3r_S0zxf zfWqD?XRR?%V-1kmWvo_M{VRkvB2S4{N7WNs?e0i4~rOuLW+({)Vhl~xNRXtzw7f8hv*C{_o zL?R?}w|t^E5XM5NkcdL(>Aa!xFqWW3hxy*-Ez0YifFaR}7kX6_GMMV=(0kRsp(-x# z4ZMxC2L56;cK;Y(sLL-Gx>Le?(@IW5@qaICo7G>4+;k^D?5DGrCn-ToJ$e;+3)luA z#(XyH_poz7cW0Qj0%~S*B;GBm?v*XYRB0*c`^Fv81J^bh`*l?V&534(rn}P2M>pygI8Bw*Ew6&V={T>u~A^ zYAz}G6c5wm9PzUl+Vd*D+&}$d@tEzK8 zn=Me(H{fkkkaWdi#`1FR#0lY&NlRw=u;_VoO}dWyD>_@9T#mS}2A@%@U6(?=k6MNz z+SwXFa1OJC#k^rz<{CP0Qo|!z5Or;sm7tiaz-G1;Thdad;C$VR!T85|Z{;Z;;WH_& zJIPiDTbwXBO`&EGDSL?-iyk~Sqz77bEySitc|n)21FU|yJln8FrtW1{4Ml<9_V6kB zyY&J}&=SI4UWy*c9-I7_Rk1qB!cBO*D&7s2Bd1vIWfpFurb$3ZRmNCu>lTyC!lxrF zlme#|CIsQDW9BK*D8%s+%X5)syAnvUiq~7u`XK|9o)MH zmLH9+o=ZW8g?`9(>O05@|HNr>%U?2pbdHjZiuU7KS!NTE)Hof4qF7y%Aay1I~vODsm^uLg~QwYKb-%7KM)YAjO(ANg*Q;nwb3WXeF+9cY`IB?zL2D-wq0Gjqe5 zGwziCZu)0pA#UwcIaSeE&wDtwPwg)7xy#B#Rsi_7?eo3@5#~Q z4Rs+ai>A8ip>V@_<%!d<=wQ|v{eTJ!Hx~x7u<7}&&e7^MfMZ%u$)E7X=Zkt<4oV&| zJ~mt5qP{2tz*maE7d6;Nd{xiuwcYr4Grp(^qSgv>uhsJMqfIlU6>q_MwR?dz+Ss<=c~8mTEKd3@Xb#-4W@k#oM!R|E8On3%~#vD%PYJG3vbu* zG{lEE)|FoH^tvX!8OI45*bIdpli8FzNz^PG{sE7mG|mJ6^3*5&oY6?;+I%GY@Ds|K zs64E`5VA@7NlG*~nq?qmY9~p9PwHuFt6q1~@6{QcJGyDg*@}FF=GR{d8VmCOevnk) z_ExRYEfUTdy)(lo&!Bz`Jl6H<+6Yu<`Hfe2rriJppPtuO`y=E?6@?mY`5J#{ zYj%a0Vop0Ufy4{FV;2%$UpOVQZ4!oZFrr$Z-++rVnb(CSOtpC$pG@Sf=6{b@c#&W& zf||aO3<9BO>x|wz78+h!7Rbz-k1u&c|C$RgAig?QPC80&UO2BP`T~Jbo>go#AIxh|Ao?F7T79VFe>q7{itwQkp=?GyMhCrE^5Z)prv>SL){pAv%c z^y~u-oXYwh(+m|4^m~r!n+4;b4)V5_ymWu}2rxY=T%V0fhYn9raQIE^`yaYv)N+1T zCPet;&(n|0CkCHlm8rhj_Rh!$n7Gnj9Zd&t(;snQ$H+9%<9@nFi%zP{ArPBWeb(mZuQ(9dhMn_ zU-@TE)5vmS*-eCMZd-wPnOY+VL4cgl-!j}OYcXB7`JrXvdMDdka$|vY5YVe@Pdl=hrS^-A zF6U>_um^vc&nrBw`X9&N#fLU}XZZ7c=@#}-jkOq8g%4`Cv5h;4YiQ;6%&HVq-WM&( zQ8w*L%08}Ke9UGa@<79PCH1UzrnD^UjC=DoQ2B7xOgB8sj2~|9#aHOso)pwRPZ%~S zRf(Q(9JnpJL^GM&hb%&!(Y|J`^{0l%A3JtzEt=%<=*N$2J$>C7BX~N&yw5^?K8Pi3woM7@=${efj|@FVJTgVsCjU1j2pnN9$OdP7rxOei%&i#W~R z#8xK$q3MYTaqT9m1E)SoES|LHkauMl;>T?~b?dFXM*My?8m`ddZK@*p3wf3`-}FV2 z6{OrYr+f?5LUBU%Ke6RZcvGT+LbFybYrL{)Q8i4Jw* zTN9{t&2K={bJQoC#toC?mUEl;N2$HvFhLA#PH%U(?~>**Veqdl-<(7F-+LbJcd7R&NF-DjMjMJ39LL zqL}#lfw;?oJYt5fL=pka=ZX`ujDEQ*SD})h$v4z>f!2O?F2V1HrP3_Z+C8n3r)ig` zqieg*%sO3re&(G0^6xs_S2NiaUVhc6gI0-JbPPFJ8Aad6sB`V4?dW2bb|v#6G7mf1 z(rxBTKh82cN?OheYaZj7*zm?5k5QFhu?IWETIhg-sL_(xgb zRXuAi<3bO{AMxgB8tf=aNu{fOrYpv@7+J0sbH6^A!}44MH%b|5MN-)9@2(1_HI)V5 zoV5II#K2LWGO5u4#C1ouUO07uVFwRSDdX0esbXJ>irv zvNANt4ax5@>}L_m($q^87LE0pbsA+|TR;1e+r_E&Z?sT)U`VZY@HQ_~=Kwp|BaYNs z<0?Z9jOEc4SzQSoYG77Cz$qlJv>Li#_hRIt=KJ zjviq98`S`?{SEUPqrUL=fJS^DbHG-G8IUemkqjCQ|p@Mm3tFqL=VG59D6ri{#3E zIkjCXD7VMPk}ud{lBI|no)U?R8vDGuQxp>Ujaxz3tsufbmav z)cj1vg@+DEDT78@^PsRA^l-kf!tjXd681`bdGdUBGJ6Ew!dgjE%5C6Z7XoE!UwGzL zP@u8VB*iZO_Zcf#c~`robs;#iU0|m8Z?6E*ufySlM6wOPnxWZ#{m%uD22a%!|B{+P zSyvlVE-_>>hK_!N2MIMtZno=o$VoK8giDR?~1hk%s+bS-h&EJapuNcpj>gM+sx`nXsuFnnz4ROx%nZ+#Q8EAoKW6)Yplg5{h z=CBEu&WDj(5d+SfUnfMvCz+K%x%1-ZCswX743vC#KbEu?c(G>+TYHWRa{1Y;NhsHw zV8szyc|P47g$X-WVy(V}4VeX@*eOLgcWk;#ZRXI%?GTjRR(Tw7xvQ=Xc)bLlb-Sg4 zH8XF!_+wq!%?(=Jgqp1_u4KxqNicn_w=ZvErL7wG4iUlJ@SWEsWX9=6biD=6@+mKqK3Uqq zNJViQ0OYSP72yY~#`!y5=ax_j(=Nh3SBHcNGm718FB?P~tGQdBpJV>eH!OVP z0zN;??s>-~RQYQ~mOffPjO4_Nic@H9^5BWR5mzC1nFqNRm;DmyOQr~D#l>wj(hoqL zVeaspz5$^6UI(i}7lyU{dQroQ3jS-?*p#%b>xfZvucl5#YLYsPLhfZDycW&AaYK0F z!x-XoPM=pQqn6KI^#t)R-TUE<%)CW1sX=DKyNKPaCdY4xy*4Xg!YR1cfl5YL`RJ_L zcBLCH5oz|dg9r|E_R{7{jtNEvHu?;dzWZEaxbu$*y5j7{HX6Znow8II{>ZN)sY^&#nF7vmHtE^P#$+F<2c$^3T}%THSm)d=;9 zt}Xn1&<9H4PE=`eeI%@}pO2L1Tv#mhXdL z)^jPxHxgHIJV9MD#e+VdmV4*sb=?{a6XmMT zZC&~gCE{5)99Vl)H4NC(6a3N(8nj_rt?fzKWjaZO1*nULD}M8Tys|RGEy@h39I7~_ zYmJ&8tL%38`^^S_W3B@E>BZ!eUo|JshNgBq48XIOuRhz(P}t;8C;V^!S>G*7LYIR_ zg2E@=f{4Q@D-7nP0_GIw6rA;OVp7W|J3XBr`^VSb8UL`7PB5&W;x_Dbh(^Shl1dYP zX0KbMoi-BJJ-msS7Q+)ygDCR4HjgE&Qaj?zMQZftOO`t?-B3`LY_PCMr-exBA+z`_ zl9Z-Xe-p-8BK8@(Qw@J+H-Fffr`?#Z#uQb=%((~W>K1qwjAw@NJbE&Z0c@OaA&|Ri zCoM{`7;aA+Z$vWoYCLX}XJ|vsy*KWNn#@|s(1YX~jya9AO-CGk8#@4=!lw;GL#)dg zRv!7|epysEBRRG=@r7 zvyJ8)K*b-m`7^f-_+tQzx!Q$7Lp&uoiG|9cGYDF?MRaF=|pF>(`MxC0<7Pbr3LP*b1uqP*$r%>J6J zmDTlZHu4NwX&(1|N)WO?2=ANF^}cvcei56dXehfm4TCuLWxYiz0AcP55WXea69ceL zc_?F0l#mywA}i5F`)_;F^y;NXnpSUvJ3q>So{+dTP;3HHI0fH~$ptoKf%@uq=k2 z>ou<_k_kB;k2TZ^5$*n=(>`mNr5@!?dq%tN8x%rJdco4|1&SN8e0ci4sH~iE?>uy>b{2sJlNXure`p(w5}{*Hf8!&0m)fmM(qJP2aC3fs^QYD+=Qw0C;^F z$+x1kNmx8xe~zMWcYg&~9ela9`W*m!ixoEgGVYsF(ui3>htJZI62SAtYZ>pPXpV)!|esbuy1A(fMYtHp} z1+xNuz|1SzJU7IDc;~F-CGjQ;_51L|ZFS+<=kR_07g8Uhj5G)oOIyeoc7^f=2x6fQ z0sVcKHQJ-#zDCBM&s;nLMGFx}p*JGO(&B^5M(<^yHHnLE=FuXJ^595q3Ou~F!3}Wr z72II1I}9AEqU;CmK@NtL-3S;v$b5*`7s#_?2g*ocMfJSmdcjb$oy`7$oFGh+KD$2M~VCOOew zg=$hTzAO4$&~LNUwls*%{?9t|&1!^L%QU(KAD#4Igi;rYl!C^yf`?P_KMYP|HZ8#5c*}xowUHQ|BIW zn%v}m8gMRrL_)ty?=bJ;P4Hb4p7tWHTw;iZr9u8AcH#>${(LMLousLMpVtAvZY~d$ z-QYw|kbDzJsve}BSD4ct-U538-gf8jd~rVQQ)YKp10Dd{=Z7#7n+p1v)5X^OF{A=I zzrAEK%dgO#oDMna+4kGMusml};6G2m|Cp>)g@zAK8R;#oSpY3d8=~sd$ff6bVL6n^ zM}CNDlJsn{+tRDH>f$=}Ag1FTwGReYdZ?=qbxk9kiXhlP8^*=^r}dGP1ghB=A)sDY zn#KHShxD!!R+B=mm~V7hW)ObDqJ8lVW<%ntMvMs(e0OH=YnB56RCpSGv+HKsX1sTq z?^amga<17H>5=$am=RHP9#jMDS*e^Z2+m*DOat%ZGNaiMhngFK1=5~>xcYEgjVJN+Pls&_~|tK*=D@D9<;M0VJJI<{q`v*n*I z-nF7;gfNl+E7Fy&E@4=I$zU(hke;gIPQTY z-<%_(hPGqi{)Z!^BV*8T4Kf^131PoX;pq-*O`FNhhNTg6J99g;{yhC}&gdiV(qIQ; zs2F2&Z9x^lbIrWHU&bRst;%hssP+OZKqEj6|^4QcfL zkU-@$*t~>$&5)cb%?O0Tpxq-Bz zY(t}WDxQ`$I$0H^pT62OV|d6ivU{%8v{ZBIXbpWPU<|!2HRr&;q@Koi(lBXzC;Or( z1Nu9Xy|G)Y{l(TG&*B`)z>TIz+%B9In7)Kq)??;-jqb$b1NsFOe??F^fJva8bRD0!J=_8QhZuorlJLdWDKLJTJJ|i z**fdxosdm5^C5o4Xf3L`(zNf&eETsYQ=OAhIde$Q3!pU12jY(8)*}y$ z{MBZvE7MRyU13z6YQMubY2t;M*?g4Y-=fx>dfso9k7cqB_~GG=2if4RzAX|OvNV}- zwc8glM*cHRoj9{JdBuLbcfp>LCi<=EhHpSujRUPA3KyOx4p8!Gii-5umE`Pro);3Z zti;<`_Et`4;11RljOBJRSBJ9>e2H)1C@qQ&%{93x;Fn&hMuWj%_qGfb zIId)9OksB;udt}kQ`l^EVf#__*cGn{skEqI0W-KBeI>&^lh?)u*4u}comS)EG&xE1 z3wYpx#a+*KJ@^FVE-2ZH*GQ4f1*x=8S_26ryy+1CXh+|}4(*Wrny70()myb)sjTv6 zAYN&m-5tF$qCS3G|7LnvM~bBM-`+QEE?u-ucM* zTcVgl&(Qi#?e%6uX7UL&ijwn0E2V4u+%Cv93B^y7T?M}xy(D7|z4bFJ+__*n6H-u( zwS-_$JT?v?{dx=J`qUceU50(0Ry|(Q&v$cRqFuXfTepM|g%B`+!-k0yO_RcRqKj(U z)S5w#G;d0=g?Mxc&eEm$i|GqjCF(q_gKZSx|oF zhzD{TK9^9z`Ee5`yzD}q31a)oe&*DY4Lm};EYg;(TJJu}&H9tzRkqBwfRz|Ic;`sYJ`W9w-*1ES?9f%O_*BY5UgZ7FvET zpJA?^mF8;tXI7AuCAT9Fr&^xn_5%=!fuTGf`2#S4e<>#~YnuOgH6I(wX3oM@os%Px2u-rIn-Y^AbRC_}LiJqnp? z%Fp8xrH_U@llxhQAOS>X9*VA*nywuY+aeBLmI8-RLmI4<(3uX&-Pt{u z1ODAi0o#Zwqcrta4xhyq3)_OE!YDd|S4kzm)Y2hU-Q^l}^5cFJZ>+6~28}$EYGVtL zpYvC-U&CqBMI4aO&i+YLIrV`!kiQqN8{FQB`7ht$C2s_rm^Jd3-k$?L`MZV&iU&n{ z{uDJ)k{L*?n%+C7-lkMo9uvhl;xtqI8^N{xv2T{n;>y-gjWxXUxyX`Xz=!}eruokI zc<*+4Z5veJcRjR}CGy{c*erNtEuYc#F{@!o;=U~8OCzSfbDCmERQ&1>sUTNtSx$)Y z*YJoqwD1J7IKdsy*0*reHn~p+Mz(P;J@&WcR})12=YjE7(Ch%+*z3U&UT=zZGgWB2z0j48rD!>np>{mb zX5ooD2CQ0c-&Tm<Pu0?5oo%)hG70rU>!wLB~5VSaO-O%FAg> z?(oJX2WCRV99;$w?w~YwHPRQnw|@0dDjf$Oe6+Q$XwCA|BuMdPB zKWrgKUbyw3L-Cb;$vc1nktw(=q-$JK7{I+OHALrY|^YZj9)3$csf`7zrXPNjVR&VZa#eI%7X%`9yJ zH>d~F52|5f9F4g5Xb{0o0I4?a&+W#LYjI0!==ZjK&*#%&@I z*TAlg>!OFyn&ElBPMNvGJ$29*os1wbeU?Sds0HuRI zY@68*5gE^rVt6|FT|VN6HYJnIRp+-3<`f`rIeFB{D*x!<)`fkRBgLXl1a6mhmEgzt zwNph|w>YAW`XEcQ{u7!l0AcuO$+{7t8g%K+9k~avb;BGL*%Rf`nKJiq#pPiPvzTj> zwfrCDSyBahq2yb~g@n_v?Ar(frF~8*)ebdfcg-syGKCNcWmG*_b3lvcJU2n0rk^L7t2qg4W&+hI@%)st;Vi0 zXe{CqT1dxL$=lSU_xAVy$=wlB$k;?zs%N~v3=I*k0zh>2J+#0+cfv^-9gRQS+{rw`p?@e@&pedD7WNF#|6m>;l_QU z-pLoJ?0hka|7D#^8$W<@TI*p66my>|cj-JLFV-`s#4{`y{w3)XT?PS?H%k-8Z4qu!aYmg}l2>BNws=Y{ z{g8^6k4gI#S3CTD18!8z+tIqd!H1!g$i>Oj3CfSzyaiOeY&SABLtHRswKVb$QPiz< z)AzwBypab|U`Wr&3Q`b0EjR*s93EVHqXv?>0RO|ZN^c!Lvvdb7f5s|WX7kesoXf_y zC*8@P*QXOa^5=sVCC0kyR$2(C@`?aMfZUGX6(lNiM}Gg)@GTpTJ&5aF)HI{KVtq;v`w}agEn!Mq#B#)N+D&Ad|`cMTtHR)c;(1fMF859od*J& z4rbp$#JN{MWG(84-?UCs`>fS-a1-`8xnhc?S;acMH&w|9@v`7h6d;NEjwmO`qdgg-{g_3bT<0f98R@IZ%gZbrp8WAB1sB<> zo)!&cop`8S4vx3Bbd6RC5B>yn+q+ZP)@?!r+{N>J>ebPyX(jyeFI=GS)M$6~QUZ2M z5_(wh97#>Ds*WolMn~Q61m8XZ_erW=eC_?=Wj2HU2Rv#jD)=D~GHSoxwyUpfNEn}0&r!G{ zNuIHWF#WvAJSH^^N6&&3`L(zN>S!(Hqgnr7?GRBLP64UL5ZeM!+EhX8 zFMO+5#8#9>cEj=i<-BJT7=o&IrYqcxSp!cMh}xyJBjpVDp9&+A@nZs5&$mrIp&YQm zt1hh4`j%*)fbMwTwDD+cT}|m%Knu1kA@D6jhTTE4 zCd14JLDPQbT^SMI@IqCrmb{Ww3GRvO0U;0~^3CrM zG_&qK1=u6{R7RxxdI|4G zL?k>-s$d=QA8BRL_Q_}|H)K~0%wUkrY+sOrLh6fQ7D3$wu%q>Z&eYg(Bi29lMZhJ> zcVT~2utvLBGD(;9~E;r5RN_)GpPG;nIY`gTjdP*C4GHf)Uh z-;BV^$jl_o9xbi)h;hwl;4|TsOBDrAo7y2%?)X_?|I2#pJ~D|r@0`iQ6U|!15Vfzz z7h?t`jGKG2w$Ju~Z4eG*OSL*-{9_QopcXY~>pg-RA->Kg4CzAi)BOL>d{PF9RH$*b z>L5Os=+8P{tM2UlQ9&n_FQ))8GBl0)Y`201(D_E1*+jWN#s{Hd8y$~^%oY6Szg4TF z{IO=`Y1L2RV2>Co7ymMaxW^xZdjy@Y=!S79ms$iJ4d9%-+%>ek_kcMd7V&go+?8mJ z62tMVc^}j&c}0|{JI1v@XW5w(i(ZQObah$mG_V^8C~bPQPfp0vgM9Sa5H1K_e9QBy zPqLJ^!^7%YGVfqdN~ib80o$y)FFWTbEW#!c2(rK66eClJLTP*=EKBI z9b7+i8NcZ2SL`EIW?bE?kv4QQZaTT zZ21L@U;Y}s&OjcB`j|_@^x2w7f8t-Gua^%_Kj_cxKqLz1>ly7Tx^(m&anueH5Jo8IIs6;-Kn|Wy^gdCfD7OgK`Dg+Ke?gpc-_de zwjN6Gl)iK6KIezs`(UT&3-{~Foe#Gq#vsRQaWHiL%(@S3b*riDOtlc1pxHneUu{r* zfpFEOfgz^~aoFL9sgL>o)C)uvKhFMO*G0o^n?jUj)5U{dtay|RC+dF77TCrwl*h7V zUhibpCz`+L4x29K>TYhV5#DN0Z!lB<$o3<*4+|BL7{4T%ZJ0hpI5O|FJ1_-*~wy05`dKrMrD$xuqf@UD*gn zIO<_vZ*@eiwvF-1^@>ii!2e489Exe-Dj6TvjvLcY)Rf>yn@Nle^T-|M@fcUcbEk83 z>G(ye}6XVgI$EzAiz&rA`0NUtXZ3 zCA;bDFw}#|Pr`0+hp5UkeDR~QvSW0{c3=i-1#nk`S^@mV=reX$e-2@Ow!Q%6f?29) zg!fHUAic#;yz4Ff7oUiB2hAP8z(}Mo@|Oqwz&2G}R=cC~G82)|y-m7fsbe8+*sX8| zG0m&oc#P##77vPsx5g%$*R4$<=ccITr5XIAli#^j+aj*v(OVt}WSw&dX-xX$h-I(B@ZwX0OT|YZvG-HY;_XQzDZuj6mFQ6MTVMLI@ zIK(9-q!_wgI)0VK=cJ#YV;uWvM3d8kzM+Cx%lzjGgq*X4B-I5V8@fA6IU^e~&dF5i zP0c^{`~9%LWqs3mbaR-KajtXicTOSC56`VZq9wZ^?Q^*E!);P?;HmdEVCGr^{Pg0{ zv+#Ig-;ds6~zjGtXJ?!e`z$)dR&T}XuU1wgExv6_(x7kZf$xfxQG0e%8V7tvn6j>cXWMGHoISmayVGulI-dTCbPns|UNNRw^^09uN0RU{HScBn@D(op|V#V~}h6%XU7>$lpb@aKb;&nLaFk$FQtm`w$wL*aKxlBYOb?&oh2Z=tH|JV+UghRvzJdV5$@N|;u| z^U24QhKInKv#JcaOCL>UQBL-wOz#M%BZgvr|G~B76yJ7$ zU$sj2X*C1Lx=ZZ)$rn&o{|RtwnPwE3z3W7nWuq9{wQ!L*^?$*#MnbJk%qXCIE8|t6 zQu4suPWfHjTB`pquN&=qUMDz+4C`i+nDNOZ^-o4w-#}_V%cDsw);d*JA6Js~qAnrT zg4r|UG3Ed@p464K(E6#ph{tDxcywdDg$+l;Xt;|**q*+Ar*H07h8V|$}J60TMLBoUB`N&H*xLMpvO1{QY>#Iw@o0h^K3XMXa1TQE!L?vo>f2W=X?T#0yPW%tponUhAx{#EUV+r=b zc?G;g<{}AXKC>tjftUdqx9yxxfDC7%;f?d8H$TMEoLIfnZf8ne%zU9{B6m^r?$1B^ z%H1vDzYp_2>iT(n?ojdEaEjc~-f1Gtbua7xeX{=JpIUdoL#oOX`I^Ez313-%4w%lo z8Y6&(9)`|%Bxeff7J1X3?XJ)&bF9vT>=5Z30fx{ty*p`NxCm zJ;RA86|U@9&%4CvA9i-@sqRG|L@QQ;KhWEZw1=tLvNBz9+GYy_R_S~R;BPB6Tq6le z6cGdqQbkDGVCS9Tn}BBg*ukUJMo0V3mC@()UQgNs@l?Id?}A=+srOh$O4nE1M;Rew zN&9NXsGr$}K5D1qu33)==f+c1?>svV0X@@bpIjSG{mT6C71Gt-Gf-w!^+iAUT@8Ie z%>c2@OV1b_lB;8Zl2(Y>Zm4UX&4INT%k-G=pZ#r)cB%Wc7*9{s@=R;!HChzID+VYK zzi9J%`FKyHSJ;lLMyxfh@o6Y7oaoCg-^LR+nyaw2RvGqLXv0M01-sq7O!tqU^Y9t1 zv4wzuv~vcoN(}!t5Af$UX*U+uLd}6mQhDo;_V67L11;WymfNAlJu6(FB zJ)<*35DB^DW^2(#907b?WRl{V$SHg}jH@%amQK8@*M<3lk+28^)#Xj@t%c|Z8<*By zTSSuyngoHiV~ zMpUGcE4Av`cV7q!pKfQWCwXq&9xkeSu20>~M3iq!pd_Z4wze|S$Zh69>)#qy*N9{B zrBn_}ae$97;kgl1cV)u|SmW?Io*<_a*uaAwR9Nh=HensUS#;w=4WIXtd2Eg2EuA4I zAVe`LKO6pGS;%^+2S`mFkk$G|2xcd9tQQ6)z@m=sji#97t;VTPxi4>Vf(~oG(m3X+AO>h>m+RkR$p?1#eKqugx!5gRJi7>15?67Y}_3 zd`pe?JwB~=dLTOi4;&MwPW7abr_B3<>UX9X+1Ax=_g=K<<2N3%7s)B~C&hVNvB@4! zgL!&16_(_z9WE&T0P9pb`zgd+(~2Q(pbJ=_WPiG~Vu2fVhI3_)O(^SvH-BX_Wo1g& zBsfFWUMspTSC=8;tiR$+y%cehH4^xtX1ipT6?dSXZ6W;IlIq&m2ujk|Se7^0O8V~52gS85eL;c+?Qr*Jy zk~6R_1(lJpT%oUT`_WWE3!^IotLP>pn%#q&Tjd>pgxH0X(51Tbk2{(Fo@9*<8mIl2 z75{$Hd28Y!FBmRd7e6?d;e;mcP~?Y6+c&?$5?4CdK*|G+Pw_>Z5o1p5#+et2?velO zq7P;J73t8o9PD*^8{xlIu!9%NwhVEEo~dqZu+c^Glx=c`3LK6i`CNh>Lsx+KuW7nl zsJ285<%Yh}5VN0Z<>g)`K9J0?;$b~Fh3L6!A#pSin1q5BN=nY-y6;L2)ww~TuX&dz zeb`E4FQ2~FjWUZ|Uz?7L@8%8MP8#T(VfYC3rDhZ))ccTxHwjQD$b!kmmp1Xr2=0WJ zj_ZQ6>u(+Lt;dPC9flf|`7iq;2@8YtXzuS)#dhTJ+b*labFtDpd<|}atBdSee5)4J zm?xt8k?SF`P$wCW8|Wl2E!^v~FTE-G+Smnm8l@k#+B3PxuBkc&Q*{JOFSMy0;mE$4 z=Cj5n$y&je3M!3|o(kmApJKGNw_0;b%LeMR$T#mpDJVAz{Zhj!z}O<+4{L zpH#OzSIOHaG$<|cfhQ^{33}^x{dLvYeAi7Vcev6h^o1;Vf+`-W(Y8hIg9nB2&+^L# z4m707)X;05M0l_ptVhk_34%Rc*@OXCg+5+lk=cB@d}$DbQCS3L@ne$!x2WlbZhi${ z6Uf2YUOZDjM3_pmxL;-I3lINp-B8hhq8CnqU>!O!z1J6$pP6xByN+A66EM2R>?h5> zOEwU6pSwv-TER_*=HvR+K>#+KyGm@g?DRU1@hg>HTN@@%M~9PSa=wW1aW!Tk+}@aa z-!fGT;0J|`u;zt14+1g374uuX&!(;0>C2Jw9Y4NBsmE{dl&yqP(y)EZH3GB5_=V%2 zXP#CD8f1gOd#(mSqGu<$i)PI}<%4n$PspKf3*AvMKI1+fw3&3UQ?y# z>s{zy-B_WJd@5QXy*&1C9E|nr&5Gqtpnfxu6_`HYE}q)seYe5C7lThDM6p}V=bjfvqTh}3QE%#QZ??fqh8jC@ zf$rq5ZCnvQfg>pjMOPT9*B3VB-zhtvn^Cn)y}*6)QjQPlTtC;4iy>ERGPFHiC|G_d zx{Prh2mlJsReh2YzaJ&%^Bno==cAHK`)GfM{)L$j6&6EloNqa8iD!ey1TjhjT5i0T zG!4?93!@?GiLd71Q59n0X9~1k3Y~}kV zxC>zTOayiitSC0)i`*7l8L(D9jp;uvzk5zIFMV!h(ZnP967y{y&cN0&S#7y(2Rf7- znfKXPme}@Uq6<)5H~jtkI`))X{8rc*TLqH6D2UP<(U(5PUPAcAG@2xewludCe2&cy z*<9`@m}ayXjnW3e&nkqJ9^SajUd7UzeUp*Rv1y?d=X(t7$8AK~BmY^PbKN;RYvNWv zbbTC9z5E;->R)SAUFZ4l_|nP{`O&LO_nQWH1Dva=eFBD4#>_yIdj=AQ#%xam5epEv z3|w3DrkTB}XeT6Of7P;1N>EY#MmTL_l6y;9Yo2&G_PDCDa6Oh?OGqO$&KA;V?*H<_ zJG%b}W%#35Pib=RRIJKp)lAu=+%AEXy`jStls8t2z3t~o<<9MwGb~5V&g*xYHC(bW zsNRl0VMl&(45Mmlaj{C&G==^rPYxg1&-#(AhSJ@u@QU30`9`B2=UwE$T$9}zVIUgZ zJ$!6!a=z=FJ<(whME$Y2al7RwR|@cOjdi=FTVHnx(lsiW(v3hQ}JofkfHzFUG*NIR9}=d_%8RAs8ZY2rkfm z)pq{G-M{LC_Qse=GrN&7>v&`TT27nIg7tsmy74xl($=u&}cScK0)f9}G zx+~W^XO>Mn6LRoXPP1y@vW|Sp!z1goCw`*_fnZ!tZXsBXt@+f@t&Yp@k>cn8M>9UAY`X+mg!Z$#a-_XwsERHDZ2qGUTGrS!lXCDA9_nFH6ifAYP` zQ7jN)^~vFgYb@z&ycfkg^xrNs!W2`vy{pFeZ`*2F#kN=8Sw#3xq>b>-4YX6y$BR+`xNinh<_aZHKE{EYrv6+4R|h
l69MEL-~Mzn{Q&R}h@)0jbv5(;XhO{>-jB)`b&On|W2 z*78jf$;~GiI^x={nGm@^8P$m}4Vm~M~)3s|Qz2(Y>UM=a(Ez8~vb5k5854QdY z>Sr|tD0yxkaI|UWTfbz&W&X}?O;gr##SW{MK~?D82j{^raX zm8e7Kovx|AK~>>#FaD5N9O&lG`{e}2QG8nW;)xCA{}u)xGz-*y5*T0TVZA^r4g2Zq zqGD)UWJ>w%HeK(atEme!49s36WlD4Tgt6r&X1jaZjmsoM5k{;AEI zZ;j)oT-Tk%{LIasuhOC?b3?Dmmp!Xfj%AhA{C8~`kdY%0$OYTbn=&r)iYyzFtgyOp zI!as`yfP?2+tkKBwDat77c%b{7nC*lW3}%|<_1J(PMZXN{VuGe$qLK-cdXbPB4QS= zHjq+WFslNW+Nx9Cd_NTfAD4M29B<)pAu61<-Zwp7)(poXN)=FO3rF6c-oq~P@;b4m zAyX0hu4^Cx`_Z0K;OvY-IN^Bkn@Fx%#Ws~p(fUCPaUoXCUZLoFnQ<>b0z9l;aK&w@*g&Cw1IIf;vwPJFu-p3;?<(%Fp9SO;J37tPm z8vM!TO8hJ@9I`jk;&5fY4@{&EBCJ5(wHQSbz8bKY?iMP8^Aq?W#=FlS`%1A6*UP`8 z+d=o?aPCoLa1WmGLo8-;i9+Iu;?bK5K&o145=^~4k zDe|GLDk~+@HMt_ok1_D6mw?JjivcF|jSz_@^AbeZjYH|W( ztFbt}OfWufH#v z5-z(_f4fS2QI}{IeTpe97U_r-WCDw>#D5eq85ey2;aX+#oj0#1mDDyK&!zZWmn$HK ztaAmh?{Y=w&MAKz%|7$Wze)b;u6SfWo6H5G&908uZVXKC_Btqc}?IkB1YJ^j+6muK$dV25bVqXxv9WO=7Hn-ilc zf#K9i>tBSTmK(LCBPbydjj0&|$;3BK-xl0GVat`i_5}zQUCbA0_j}D)9KFj7D*SYH z_S;!}i}91b$XhFePP%8YBx4!+?N_{@h3<*GduXma|0w##b#FJE27HQfPkU;D-#E2` zEK+yx7VcBoPV?WN(e!j~k;6$&b%jnYw>n2*TxBX~Ozzusqyzu>xnTo!_|#P1$Xu0B^&1Z^ zt^Koe_a);|kKx)+2I<$|cpLV?sdv*bJ|*lPWM&GP>-KQa&Cr+eO%y2fQZvc#m0 z{92+3hP{54AYs07)*{T6=VX~NrsK}__wPSHIyd5qJ2!jIVo}n6zDIOsdDkbZ%S}B( z?krHy_mWo}V{L$&Qkp4M-EfJYy?@lxLDap-@+3XO&+Q^{mY2zIqGX5Y_^Bf>MO4o8 zTezmmcpIEmL(LFE-Y)hTs7O~$_ND+P5RfDv?ZL*m~+bYW*AieX0h@l zoBEEL3S1t&MfswJ+Sc{WG}j4XiUjV0OinT$`4z@Z!?q_Zgaj-Gx&;Y}SQ;%?X33FT`ZeE4zkqVCFN z?F&+=Yh@M!*QPvcZ=V>~6~OUQ^#F>6?*jQ!BwuZJO+L(41T<8UF1&HGEyj-UGl$Au zYz2mzn!Lk?K3MUCo=kbvY<>TJ!8g8KOEG0P1ZE;IWBev#r}itp4pXTNB#a`$@x9v9 zP!=8VqFC)y>$v~lD;H&Eir%xB#vPH+xW7Yw^idwgt$68R?J4w#L4sAL7eB7cJL7AN zM_uk_`*MIwMaHDn_P>XF)IMqr7QaT=*cf!tr!p`0~X=+j0CYO|wd+(T|a7v5S&O3_% z&$esp0oRa^L@|ZE269K?Pxz^F7dbxT>4K#thkvLq!`I9p-02lO%E2%6M?h@i@5wY^ z)CH6MjaLXEdU+8{&$-E>J)$rbbcJHuG{SJf4>un)8*Yj;y{>!t?i7-RDaH|s8uplzL&?u=izJ;f>fpP1nbVuOSaOx!-UpdVKN1D8 zGy0=!a&zduo@mvydGOJL?DIbI?+CB`1^T|q=3h$ge0uwnkGw)G==46n0%t;Wve-V( z{XF)xtw4}>I?sz-&81}5I?C_KtSlMAS+m629Qpi`2z)O36ykuS{Bni-B$f@zWc12) zO%Pb(y5>5(uuPvJr-5+IoiqILInyiaQ~CbTxFl%kEGkC-7@VLx814SI*Y#h$vS>UZ z52mwRm#k)EP+SuU{r26pec@=kTY7h4xH96=(8R;+U@He1TDO2SMr3jn)&qxM+gvAsv4IUlt-oCMtdJ*;dho_aA&^y9M|mCNHb-LD@nnaW8%?fv{& zyt0|QiQM)aE5OR(WBno#h(A5sQW{4=$Y<$q0LDa<@zWRV-~N-CRA2MxHiyCwE7zj@araCo0^|T4}J|M_&I@#8nd%%va^_4nDHgHwe(_yMt)($ z;Hd~Y&w_2%G1T$o17daOk)LKJa*8jb&$M!l5-Fw6{@h~4D3Zp1Co(Q;~NlD&Y7juA9x3ynljX+g}U{ z<;uTQ@6*RUpk_CqbhlU{M8vPATeEOJ{dI7Xty)5OGKy7{D;W)_;qy8!`2wU_R+{*| z760ltYWe64JwdQZKkFZgvMnm)@u^t8h}}nG!-zlte)tp~ zuiI(HAfH9HbfiCu0e<;O9XlJNI_#?KtUD^(p4Sh#F*IuINo$jYj17kguGMUi>G&u zYQc@WU;O5k>xZ%<1u~JZJZR#gOjewrx*(a}K8T9UPmGbLVtvQ1i7Gll+_))sPvCh? zxE~y1p~=GRnIZL*un~Zp1?cgQb1qwLHhR&T zRIK3-S?NxC?PF3faO?L+K3ATSQ^bp|JX-~tPT>QvV82oOITo{SsgE^ZN7=Z|XpjaM z?$ZkxHLcM%bvgaNh7wJJd61!L6Ix<`xG%YB8)*{nlZmLj5i}aq{|R?vh<6f`2=25C>+x&sXXs@(mSBEpTa%zJ;C( zM-N-vP_R{m%hEX!N5h?hfBBq5N(o!vJE6`cFz=~x()!bod78MHv%Tf%#coqGFk8J# zu!?Z|Q;a@eY?fDv2oxlGnF=w|85bFpF7ve*(0JUG0aTEFE<9;3a5~#){k$(bOEWvr z8F_wvdD?$!266XP{Ud)P2KSC_s+l?i|sJ+xCYrY6JU@&B^DVA0UUf(Clm;J(-BR*mGY=myd@VSg8on*cgNqtxvi?xi4jLpz>hD{k^X74gjN*?z%-nzWB_)z6>)C8{1A={j}3UbAYYk34W zW*frfMnRdQRkqVwzobt7+eSdWXq+K3Wmg(#BH2@gAeR=~Evex5#QGQGZAbaa80K4J z0+CtXU)d=^qNnX~3j z^cLM3&Sin~d!RmrWZO7Q_;>dNiOMUtScPR7Wz*pz)kkZVcfaIU<2;5L8mdtq&?e}q z5x?g(gCp2`R|P?J=QYXdHm!lRX?|Qg{Vh`t)su=C2~E?Geg93F2EvDDr*l0yHkYxw z0{Y3W*ed1zm1|1e; z?g*NI!B~%fR2uMSGq`Zn508UA$yiyBF<0|UwuUWXP4%54#yIBrp5O*07jXGoKc`K44Gc<%>P2Stc#1@&0Ul)M)ojXO|4i(Ux~$%h+m*O96}goBHg#cH zVFAW%Mo~Ab+RA&<@>jpp$e#B0{7XZ7?v6+4Yrx9!Z}(uF`pCccpKWCkd@}KdiRJ zQuH)9EHL*N!ZuIXBAEzRjpnGU+CzeIHj~O)qbV((Zo;%6)Vd}xHoeI=Kd%?}+A*#+ zUxYk0h;~jT|XViiI^Yog9ff^jeh@ioSG?f41jCw99CW2KjAJLkAM*0j@eM`=q4yM3a1m> zi1as7ir>mhsw%qx&ay?tcV?2Y#0}$oj+e~7t?A9wnxQ1GDvt-#OEH8geXg#%-!)U= z0dOe*NGAP`N|)xTyre7Tr6PP*DT%ENUliD~e=ZdUtZpkldZugE$%ElVIXq{-KIYDK9E-4~s($BXvl1uFU z-i*;47LF(D=V=F0h0G(+o{0fZLE#>k7{nKOnX#~y93qi2kh;hJMR&i2$+#tDW_X(t z5P{Y|fqm;x*$YunRZO^BoUk)tupk#KACGs*X#U~01H?J%*z{_QcL-{@_g}GuI=mbH zt4@Bd8{Q|GSvb&BHU9Kp96vaLfoYyi0y$KvP z=$MC5B(}fB;y*3B%#uGEq4^&G$23`=?E#6;xw!PUu3xphSAT43rH{t=G3@W89}|I! zhx?Y6^0KIMONl@MR(~v-Nh87YlbPg#u>!cz4Q}IzB!rYW&nJ>xQ;MTUf*$GFUA>4w z4N>6rTR!lMaDYro=F>Z*Kn?XI1ozCYYI*I9V}!*`bfnjaV#Ec9D8><7X7&KTLcHcl zP?Q;9JjFaVDEfRF29QK=7oAO=>1#K9_nm00NF9`#!1Wp=bC(jq; zkNI>M0Sv6ksGBDe9}1F6G2KkJ4K;$&#P+z9{0E!=JY*a#y9%!?Pg#VBWM$tsO8;Qf z<=CE{-1)waU%Xr&v-aqdr$m&~BzgHfFPr0->`ap z^xaG*t9CiV&J5t{ALwiPAd}Xus-35@xuvy8D1$D|HWXZC(GH zY#;QwV46K*W`=)-3pHv17$h`gte(|NTxl?x>8 z6TwswqoP9-Ze*QA%@wcar)RQ}*RfWVM=x&k#=g#x)L8U7TZHsF(d-pn5c4{BINOL= zB$8((X&tpS4Pf9J>{897A?t-#*54SI-_uh50yzolqsK`23lUbUu zN%|ouv&8C(koe@J_enKMS<0(nRMU+T_##y?9AnqNY6)@;Jnz9g>YnhgPi zSTX5j7;URU%kbdkvuu9aVdl7xmJGjs7TtpRdBgD{AiM<;EJn4A&+sd`q*7R%QQ2Pi zu;g(Z1Cfh@E=}lYIYx-Yb)lCPg-XC%k;k9`VT_sD4wI8kvS&?*G1fLr6zGP!6GM)3bF zwQU-nzrY1Qti%ct(}B<38mN8$&X>(dNZt}I^cLWI1AnT5tP{Ug+JIFSI2@cZ*;#$G z_4?DX_y>NDPhP93@uR<#qn`!4HT-j~Mmo0_^b7NiB> z?lSkKOFx9#U+r?()sJo=JG-kHi)nKl&KUIWZv-T-xw@fhE+6yLhE;22a zC4s!WKBYOq+`lmWASR0s$w-ghhcjA+Ye%2_9@GFJ7uwvpY@NC`?C6ebWvGvXdhJ(2 z*4GOy>&`1dtv|M0I6hNY9+D}T@7|i8nQ7eG%-!BOcsG;S&@I40e_-g@W?gg$tCgC$ zx1?=KmFQG!?7#66NbZJ~Z%_@+bcR`^h|Me^I*QhM7hnE5+ky7~15hxQqYY9VJ4xIM zrG-D(X7+9uRt{b?##9e;hv<9{>2vFxIj7>T2SD)y+&l4<7nS1`hCWU?Dk2- z<`p>S8vbGQzq!}|wsbA3h+%5G#jCr&TDn=-^T#Ak$SKzJl6L#yG0*s@W9PWPuF^tm z_7$Cqjfj~}6H88j5f+$!^`YAZNJ$GG4fp4efXyoWy1~`2%e9K7z3f8p!7j4H#Q*uy zxP&W6(b|V9ep_iJv+zUD8AtB_?n!(^G!kxwz+~-p9yw)=I-*l;^DcSR*pyo>7VkO zGkmlGu)U@h?UDT0^Y;_vm8Se*A!(|NOKg%QZL7#k-Ej1Bp$Owbf9?|NiR~zSb%G5| z#QiKP{X`cq;Dq$CPRDAbL*IBa>+kegicK)L1lw ziJ1hA%H=1^m$nEb?wjmEU+>}GM*kj(-qot930v<}j@#1f+cw)@uLzEvon469{V}iA z>35}=Wkcejzx*g9cnu}8|Os_*V^pV80dXTLk{rT{)xI*?hEO_{J!aIe5B_mLm^%;xs`O-i%>1`u^)ViL|RN9(-s}{3LRw z3I!$=5)#9xtcE|^(k*b7pvx&Y!>JToMa-dps7CeyeVXxL`fEmwzER<03Um2H!n;5- zsaozRC;`q1+)9t$;8D6&0Y0{P4woA3%!&-!|NE8`D1(z{%Uhrk6XHn6I|5(R#PiFq zNmO{$xL%lbplnZ`I&zJKatdhSvU(%&6op1eMcH_X3@*=JdH0 zr}T24`W1c64>(PpRI!C;>gx<;5h6T)NOFjf<5tBh0WwW}&VZ#ztw4N^vzGEVscIyz zmLluTK-go<;Ng4P+(H*xE}|c1(|+=G`R;6@D^*>&DE*X{y4|nWnFzm6YN_Fm*Tre6o%H|RaBw9C;3p-1-4lEk{z16)kWJHt=^~){ zb5fHreGxuZjHFWUj=P&Py{b~~#OUYn-1b=({MPgIN#w1Zf$RbJm4Rt9H(}`gIUlmb z`Qrjzr;__W-yYf6Jxj<-u;-eU1!D~jx!l4cW^_q?)&t`#R?EeNBkGF!&WByOWqh)e zg~3F;`-3r%&a;iL9u7^=MpgG$3eslDAo5U!abRsXJR2026Li_jkT@Ii8Draw@CCDV zvVroBYnT#QuOOLkbq2oSmR~o0?z@K#RYUUp>o@;=$yl6Oje=2B%zVERUEgo;8J6do z_igf7EY1Bl*54KuR^@QH=J|nl2K)Cb4iwPbxsd0X-kfs#TH!~YPaEQQt)D->*M(WW zdpuh9Xie9Q{(Ybi!=OG{66Jv1^ozg$O(qVuW16 zvj>-k{hC&+6$WwLG))jqbDB6N+lB9CRvcTc&<%LgSa;t&>*(})h&LGZvNS7V&<1J5 z(S{inO!;jDTyn49G&62jiCfbL>&W=v&Ejch>|@)KT)tUtUJ;a{!Lvd_>uKj-G>>j^ z>?pEN962%9kQi+zAx|wIzE2iBe7@>UO07?Y8vgTke5C=>y>K5R?ATSyQ`0@N5&t?i z{`Rq_;k-Sfu|(*ttfZdcSR1LDiJ9bbqb2iY~&POcF|H&?GH?ukt=Wi{3CF#lxD zGQ`BpiHh625BxIR@^2dJs|oyE>m6H7P)-}Ly<8$7Jaa)*=LKdGb!hQm7OWi@RW~xp zF%9QttyUil<&(9V?pcYp^0$d@+qW68Zh-ypFi8Bx!}{AtK&%=vp#j`CfqrY+8MlNL z0fiQ+Jl7^Xi>q)WX)hI-+j4flz-$eJAeW6}n%&sBJk`#!uv*Y)Rv^PzS1exQ^qh)o`) z0oBgZ*fuWng9DHek!LedqUyk@m*VU^7k16z-;BZF3;7qhhd_3Uv4`R8*6z2E=u5^B zD0#y+%^$|qmXbdx8yMvffwpybsK!5HGmJSK2z>$teP~6yg-YDqUd+ORMsHc^pKzV* zI=o@DaRtz{3*Nd%KE9&JcxjuM^{8^M@6UssscsY7WV;x*wwF!(_2F4zkyD-vus`a*sh}Cr3Q*h7f`AhMnF2t_bj*)rj{yh=OXh-(o#7Wzc2DxSn?ia;V_%=7c3zet*{rtc)_oLr0SB)UI+xG{DcACLFKJ_{CR8L4szh2TAfla>N zRJ#7*mxVA(j?{PZo@g_Fban>5U;5e=x*)outVn#~m8`F53iZDc-z`8LC!Q|tC?Ae( z`jG|YVof<@wk6g+0zq@Ovh3w#iW~qt8Maehya3S_l3u4>lAE6?icX=gte2>4W}pC@ zzb`_p%C$sN!yau_vY;s&<-KGrGO7E7!`{VEHs#tse|atJ)E*<}W_{{+p)M~>p$A32 zxfAyv|9tUHJ=Ur*aT=q{`3n_}snMzvKX&V=;I(Cq3r?YsqawewdekGz&WoQmpD}YG zRv%ZKI-)P$QTPUE9pHbmexvR=V!UQe7l?HRCqr&#A!jffZFIfwQo>o(O2du~>YuS} zH51c>fi7^C%T0nmWI=mo;wsKET6!k;yQ8}AKFuOLh8*giBjHZWG>Pqhb&Nw`=w=bMMMRvE4UwPYTMfmKtheq#=ZY}D zOB)H(%O~0bMpvTX#S=v5W6QUCB>ml`j{_A;Rc(gZMt+~m8d zg(VhfM37%ijPRTID{j01)oH*`EeL0~Bm95-5-{6uOXcfgF8n#AAaDQJNs@P#CFQR( zR1O{IqoqvS=K8p30NtF_J~FHncrSAUoc%)fvitLz0dbOzFh+p$SPejlP^Op()W>V;OzJ%RV<^m!AZRB5+vytJTH8RT?6Fy9>n z4AayMhY%^Lm19WoUvm83l-}o=4#A9Bmsk1vY5b?On1TF(OWaZ13P7*FL&Rx6{RD&f zjI!Cz4GK4@@8}_S9{6->$2$v*d3jn_zRpC}SV=>{?`nTLv{k#rFeK+DCDFT(lf7+# zLak}lHXa@T?HL|Hq?jQCP-V-mN~;VeJGMQ%evCqIfIf|c&$XTwX;yHbp4u&O`1g9@ z9E18VV_Hbb+tywysnDpHOjst4JEtDWB`apMN5eC>3px86U{LtY`C?Sv*IN<=xWIhK zJ*UtM>ef@UTyQm|SnGERuBRqHABsqoub!N_|4Fb(O&Uz!G)ZV*X*e7P?q;Q>Rom!| z>%Zl%6Uef1r@)Bbil*qL`f+u}i9AoyzJTxAP&nKs4!g+ST7-p%?PpEIZ>><(X1>{) zXx^c%G)vtdF~9~+>bD*p9~v$Z>eo7-wn(bH3z$qEG7(b@Z_VFwq=4E%BWr9 zs*PYrZR19 zMiW1UrrQO2)i%>yCo@X3X+9roEh)zj)(AphZK*u&>COxTK9*Bz?lqS|#yzE^5 zJcuIfQ5&0yerj%c`P;_^Ay5+^5@QX~k<-10#Y$%6wxLSxs8*A%+w$?*7?PHDM?m4u z=pdVthBs7&m!8bdmV@{0oKsMgm?2czUA)*t^P7jSQ$$vdf4_#MPGsENkI2M*tRf>e zO5xuRP_RV64#)E*G_L;->+i=0mf=2~hPxAHjH#(u1Cpl5<-GojB~vd+Kdg{l3!=8| zK3`o_!cn`#IVnXqE@P8`9buuq{f|i`m*jKivETMENYE3`?D=$XaS9W;xIfk0`2+IM z9^4C&yJguUQKUh8y0B_DoPxBA8oJA(m|s|z0CfX zr3HzxHjJo@gZVzj^Be}9$Z1~NTDlZ|;^2F;pyX>aW);@!jiE8#4k{vY(HGWH9`j?i zLGF*FYegPYAGq`H{mxxp5c_{xE*tN9KT(IA5Yo@!8VxmF`RjwgOqX|KAXEv(A+*cZ zKr&etlN!0h?ZTT!irr=TUEaf+51L?A+N!eFZh~96zUr#y?}j-&bV(L6c@W0!_RVgo z?ol5n*4;BPOppe4u_m+Ge+?6-b?g;@J1r&CjC-y|wjai1{Zl3ov(Ix0zC+%}N=P?y%woKQmg?dlN9tOI05beT^+aWk6NpF4@A)WY;wq!Rw z4Z7QTg>~>yg-m_7?Dg7ilcoxgyc17-ST&sNz&xWOS$i9XSVpN3>hKwhd!Ug--Ox1W zNt(s+@GODtO(}l%p#j$- zKnZ-0OUxAH+sO$sZYQVASIy54`9H`?N^NpLI*~SbXf=<**h4Op7KQIK(nOs_uF@uK z70nNfZq3Q8|1(?xmsG|Ddc{C}4g@sV_R!KiplNURF!a1qHbm1e?yVp6$-K0AUOyP) zzWDb{zTL{ST1si7RxqDij8_oQOY`xXl49W^!#@fXUAKQSOlF?#dS|hOyh7aEJah(H zP6!BuMT{=jrS-bp&=HIM-34(gn@(|;m7l*xfKgU@uwwXacG1|+-w*WUUb%RdE=B~s zwF3M3B4t;wNsB(YOkuHM#3?7^>7Fh5Z_BVGQC`r5)CVIk~No!I&KLy%f8DASyyzxb4JD4 z!Fv~~hxFbN_5>eLG;Tt(e(_nHu8D!%8hv1k=iSHNnkmdlnBn8nI}_K4l|5!k<%Zi; z3ZaW%`c`gVzup~=52HxK#XxA{4C?J*EH`{|kLAsYf!d`b^)qYr?2*z?LJX&T?cS2)fS z@zxqp5VUm6oUC`Q$o%&NR5~e0WDcTds(C6dyr4w$B#=KsC#HdH&{Znb|8c^FyDefc z6F&VA$t-i7oxyITCH%JVt#Jm*_}rgII#AecCgIOMPpm5U~{cvKxRcCf>QhS1S+{F39z5f}pW|ws!Ex?~9h$q2_gBD#`&pNDMz{ff}9;lCogqW9qm@;rIk_hWES>hBaa z0~a{aM;|dAF11+nV)&`ttR@#`6kP9*+6FDmauG58v(}|m2JU50I)XQm66P5hd;GZyfYN@8THMaI=%tk7{0O=jnN2|p(7E~-V zC#tg6S%uqF1u#XTf_vUBWd6^&l5Gu+r6yOAi`f28mijTWhd`I8@O#g^t~JE*#9-LM z!Oo-FMDyp26kCPZ6{>VxLTiAGh_Amw*%M1M>mUs4@e^PbdE?u{$L05Aeh+_=J4gT1 z;mH=d!55*|nb9@2Im8GpKnX0_UumbN4{QS4=wA3?v^Ma40|%7pX8qVI?Eb?0=~%uF zypPSn51aj%E+`+M+c;;;Bg_=ZxW3$@h#lKDS;V`otryD8|8G|jRhx0YgIRBz`%fj_ zbX)znd&q!zzMY+hm95GAv-C8lmpm3lCTBBi8oyStJ!Mw$lOe#%$l>f!R%9VUP19uj znf`qdIgocv8FsL%EADa(AmD6;vVKD(2m*QxYn38R4)IrG-MC~pIkpPM`Yjo~1Kg6& zB1!-InQ9(2VC-(~GZ*$cgU4*kfTdELfUEmwYI=kA?2Ce$nW*t;kCz1q4&~ zX-Yhh=rKibU)3U08h-qvD!5QWqkpUd22jVwndK>K@!T`e`R{mholTQCMQo4cv|;55 zfX?C-RDcXw5`lHEsB`1d|L)6=(LW_qmepunvm21z)NiAb5WWb5IAv|y0bmm$l6FvwtXAt;*u#eFtQ&BhnofJdW{dj2FK?n)$p3vZ2i*iGiiegzkWm1k%_m;h}|JVy} zDb>3bjfyf0$g3;Y#`@g(chI3bqx9#{++{Ym*EiG!#IYz?lqoq z2{w=`r5zm(kiUmZyW^+#=8=`dv4D~P?)L`3^4;u%=T8;Go4FT-sC5>U*Uf~iq;?b@ zRM~v=vir81XyIb;!R|bdJWx7c4V5U!R5>-O=JzI5@H@0%Ami~@>DNNdz6(-23a!R- z&Tj8r7Ihf%bY2b36bVUury`}X+XWn}(J?z@3#Gh$EH8?yp{$cGC~l{o2IdJZwPJ2K zpmXNn+TjArx3eBEaqo*F<_B&(UhGvcdv$X5 zNk*TEaWFsS#F9n>5>+zJ$}oEBnjw*(m!0@*IGhm#sQmZn21Uh4?Qi?Zgky>uc=E9+ zsgb=7aTiv_LEq;4*&@{LeE(n52g0UKhh>F}DXzu@7(oqZ935BHNJ)5^c-Vn-S&TJB z?P@tRFbZ}z5iRTuy*-UXBix^FWE^xK{ebrs{*fq6=vl8vZoSk+>V0AnKyIfvOIe>YIfuoExPA;!=pUd5QdhjVt@hl}3Fi>zXJ ztIqub@on~3<#o3Qvy41KTbBO2ibfSvbojTF=C4TVIY!Y=1~Na|>?<-1B0{8(bnK1! z{99$e8gpMa6Q)E4zpR>R})A$d$wX-*<<&f(#up$_j?U7LK!_bBy249-N&!6G-D8(|~*xcZ2(8e;thnP)Bw|HJ2l|&c={z zX^xqXzkw+0kz*5iA#pW^hl+H|@uJ?PlK>=~bJKUVr6Uu`Epe_Q={lL*aus`^wLHULv3B^u6rm!FS^>$ohx*v>wq@RT^_oE*3&-k-lFY zM1bz=8m4yqV>T_?9t#%92JEkI{9Z&Ftz~QF^)eSZ?8M^cXM33dXW+!l!OHGJqhaWzPtOb z?$3R%|MguvUTU5;_dRFMoO5R8Jc(YITk5PFhUQc2aV_MrQMY|Yj!YIEK|5R>%2F$o zGDHHBD&<^PtvOROy};+QOB1)*?Ads=3sv!_TE+;;r*<_2X6r zlYYy{Tgbl6`mjB)!eWZd7jV^ur{422WfN4X{SWVMFXc02z|e7rH4m6rYk7FbKU=h+ zUA`t)tXs#HnGryIU6;S{DZhNS*(V&+3BKtmI#U{fD<)LPuAB zEG`3ncPVK5)j9?2#oi1=%Sp})p><{yoh*SXP5*SS2!kZFC+q?)l zd+?$7Y3a(258<8)@wkCtX$?7p4Ixb15Jv_3@@adgsvrt~)fIb<3;x4cZX}dqwUk~C zd8pxopayguS3(4f3?efgVl*ZY-mZ9YuRZFjG8jCe;&Y>K?v{(>y0YhiyKBak;a6K-I!bdV^tgQZQwLRZUbM|z zqEpDpUp@|laDq=u&4Ob`wT8wUpIfxVC-)GZ66YapJ>k^Tp;UoQ8yL&zI1n;%TdKfo z&oVjK3ZgZ15R;CZBDR!(HWMv;BYN?cLv6$5Z@X)u-S?++mp_Tg>A_d|peAQN9gDNitW$XhqbQG|wVhP}UB3RI@O;-;(dqUo=8zcAN+#*H z=L`{fnZvjMOCq)gbzF=1krDhZv2}r0TVmUix^504d*In?1dy&9G=lJ#xSV+>kwQZu zf6ga^VVJ*T7O8H8>kfX0cO9Oq3}T8drizEShCn1ipY&*m3=3uHVIirg#0}Mr8o>$L zi&JSMg}u|WnAzv$iT3G3e69t4)~!wR>2XTt)3611$&i|Qeo*Svda4-9^0Ey=nLzsWQRi1(_L}o9|l7 zHZ_!>wiQ0W1YI{Y=L29?Vjkta$f_C(4C&OtaI{D=zMJfoLFAR#y6wahxN}3FctY&U zZlDO72NS1f{eeWwxw#njjzNxv>~64QUX5_ z8dK(eJRDBftS)D?3cTinmZ``_P_?3WW2Y$=9Q8B=7n+G=SxLBW);W(U>=%&je>|L4 z<$)=#Si0ub99Ecyw1?KPGUeM5#ru6K8t>!i?G38Zx z#~Tw?oxiwE&vDbuB&_o63l(sWgERzoSjwq3L@0TyRx}j@oNZ?J}N?R?VERRi|(qID!5*KUM>#A6avs~Pt*)^PY;g0IIh2p~)FnvLJ zUn7(-@ocs+RBW0~t)9-H-5c?RQaw+~@nzvM=(f^+Zz1Fu;E7ol!UJnt2Oc(WB%jfk zSg~`}_=I7jLvgqos`eyuQQ%^CdgsKyk(UhdS;9A)!B^8JJ!%TVD9i8>J(=P;tBF%E zl?fkU8=?{C_d)p)7AnG>B4-;l?wbwngJ|4A1yr;@ z^M08En_1V=3q4lR^BhcvSWJ+zB{=j=K8tr+r3Ah?sk<#@BKK`7bG3&6`qgW~i%-Wz z24*`y;#I?&2UN?JvWB}77d|`23%%j0!s=ZOgQZ-CRJ;<%v<9Zn%~j&)BQ7^ouBo+O zUi&Z#p3$$Ad%-;QIHKWI(TM-F|LfR3HsCv_=o+f)w9t;78uxn}=8qM0^U>=0l?Chp zvNq`*sB88~$cD+fhj@q&gmpHw?5L$$I93-asi|}XeXV~7@fxtts9jK;H{fK;SZYhK zQsu_lmQh@vik);Fv{byU54)kI0A*28ZtB_*ALQn`@9e;O8&5gb!?6~+_$GXS4ek>X zQcC#DmOh775l9O4^_8s)t*AsZ*X$%7sr}RloyJeky=g#=2veb284hvD%^yRT1yn?Z zC(Z-7W~$Ieydq+a+v&ogIv2LaHcBNV0l!=;` zd4MHBb+0Ow2n`BvV2fc5m1|3s^>emInybc6ue)aC>U(g|mbH&{<0=LT+d6WC6T22y zR{|;&mi8e!CXi387jrj9FV4|1y;Ff(mf`h6p3S}qzifzZ4aE2BOp*GSv8o0I#-we3Q-#vS>K&3i%sFt$;qcA*Vu`M_`Y_40Wv%g<&_$E; zSy76z%?0lQ%DR+i{nHAv>KCg#{4Al9Q=JnPU3YB3)XL%5@UkwmE3`Eg4*pf@?!dAV ztWz@^j4&6KVv%UeMSg&-1f|b`E$5Y?EE}nZH^bj05pRI`ImO?+XibiklQ^;pEF_;U z`&@@Gwq07-A%=~$9lgKB8Q?F-L`n(tOuEd+;U^=pVXVDih0iIM0(3Su zqO-V8+xnZAO-+a=ATAf@p^ThYk_Qc7&yFw0wrSEZLE=+`eFA|&^Ne5GZaiISO}=zx z=I)AbVESyu&G{DOGy(S+^|4iJ@0vC9a?O#CC-}tCmlEjo;&O2b-Gw&{=flh3@Jw)( zcOswb2JpRDt@M0@!2VeH^2UO88v!552f)A`%TLSqn3`x?tLoLbX2q+V+J7t`SYH!L6PH9uJ*Nfybiw2z(hQb3 z89KgpZ`JRkEUGn%uciR_`kfkHc$C?ourhCJz{WLO!fcgzj!vStKyE{X(wpFBh~eh8 zY($iqzIt;PoW;9VIL^RSh$T%v_mi6h%rNB* zT@zkjlX7TjI|ksp0|WqC0?Kzo8CXuxu2ODjep-Ln6S%1ibqjT?7@g_z-dFm5z^7v3 z=In`r&N6%DHkG2lC)3FW2%uY;r~2P%-HeD`r3>y$aUf0S9?sy!cwrM zcY>dPuI}UY@fn=34`~~|0VadlO>zpnW8~u)V{zcgi}Qn8O^Odxj9gqa3&|`l8a17N zQ)tQz-`*v1CGTyw(uPoY*(_Rlx=_F{4|gE^lC<&g=5tTn8`xSQzIiVAMu~R5Z$lI? zECvnJBA3@DJi-AnA1iPXASK{_;>%HXX_?DpF{RyC4S8e$1!LMiwe(HS!N;NZ8w7Is z7Xy0)^J9V+H4`6Xt2Z%BOy)V~Jc|`&UfhQ3+#}WCo-^IJV`v~R z?ikv4LM4ZZyWCM3SnX^3)Tl-G%&^Rg!0K!#*$$~?@AqLz=}N@vsVeo7Wbz&bAU4dw zyqa!^bcfZ=d5y@CW+RER89NrY;ZH3e|#2XCFpe<5-Si zNcgZ$N6tpJJj_^0|FD5Ln(RvBk2Am!|mfSlD z43l!Jb-|`e0|rdYK{=L}#WPNlxi%y3J0BM66SAVuOkP4RonF622nh2iUSM?@I;mOl z@#NB7d|4);ZG8v&V&kX*4?Lw|O*nj}7%SLP8E5fIquvev0gf`-e6upsB{4qr*v+T$ z{X`N_0al(Wn^84oB<#t9`I6ayYYAv5gLY(xnv{(BEPBy;qd)@BX)}EM zpxH!g9&E{!>d;M}8fYW;>%3jiMtpchC|UYl1#nb7y za14l3=+GHok6_PpQNaQJ!69e8Ltq|uHm01>Ah03qTqFz*cMS@?;2#hY3HOFM!mQ17 z_D6xZ!Tjwkjz)pFk9zoo1|y<;{5_*Xf+H^m#`$=A z1f6qtu;;u0;$b)*>L2VI66)<86>|m=6OD)p@IQMt#M{wB8@Q_M6B!tW2n>vYhez2v z1V>*AfQ9)7hPZ~h7z0;NAp#>~F9stbPKz5-&=zYrA>=wxqW zd4x;H)zV;B0*KBu`szi8la9yFgFuwmK(yK}PDWPlT(?1#>`rP#P%G%){{FYW{~h1| z{~h0cie4Td5XCl=a;`%!~?yct%fg^JHbc7N(qrdU>m^iCN z1p?)MJ0udHvQt7x4}UFfVl;x7uLndT;f^p3^{ubRJqhkA9F!p5ancDq5ewhP$pq8~ za$F-4pL<<#mtmzK6QI?PdTj#P!OloR2MPr>D<2OX#n#2GQ;B>v=^qwNjn3v!iXJyRC;LuT7A6vd-*m%|j7Ks&H8sV_!Ebec=ZCp_>YN;44z?XE zOpIHKKm&}19~_V*V+%Y3IyL*9sK35#C|Qpi3?>PI)D7ej`ArwUPX=Uo5Ec03$gk`D z{?Aj9O1r@vWCmY_@FNUJ@YEp4)z0Pbhy0H_N8eph7TR|544~dkz%<;Q(`zMgxff$taTS(Vr6_!Bc@mD!;e>zq}8hr^mz1wZ(vBk^ln# zr4PvP6#PFA9^f~1S(KZXi-Qdqj`Z99f&hR|MMZJtFI)9vH#}40^Af zZGaN~hXhE?rv#V$45&Y@12>kg@|6Ht2YxdMwxl2W$B+K3vxV}aUnH;{u*JU_g(Q56 zegDAF`wjOyQhUh+NW<_q1jx;&xcHl!|AB*5H*pdHU>Gt0rubI^0Pv|P#C|y1`(cVH zk`iJ<0^A&|Or%lxjlfp#!+*HCAAaDeysU)SZhkH{7Qhbu(gP%XO5WBVuI`5)c(0`> zb3jy(7rc|y178KO)qKA%6T2U-^amc8wNp`)78eFofYbwI0RX`Wx%upkKTyx#zBN-_ zOuRaU?iYq|e;I-RJ`KnQ`;%e*v^spm5nXi!NijeJzjOd4H8tzIKfV8_U$}bg zh^ErPeL{SI2>_e`6*=brpBSI5rsNtNBXlQ7?|?>&juff!H3iOI%;wUL?GPk zzz78JsVO8r|8yz;d4&Tu$8|LnB!LmgzU2d`K_>`*e)~_qGGJ?Ph>bQeK1vz@yZN^80pH{C=ePgtE1SjEr~d<= zmP(SG>F~3m{;<9}XUqSPKQ%4g_N+g=_a8WkkGHfkHPrjH{!s&48vg$P)E`!fjkK~b zF(4UWk_RM@KWf_TH2{r_#YV{RH%j7Ns{@K+6xT7E&l`HZ{O4X5481nRpQ?IT9}^#nh%7)B7phbGCzR+ zG0;c;?&hC<0@t2k2f!Nx$(@RT`{n2QG6y2zGf4f-wf660i5Y5n^DxHA+`angW~L@5jSY_<)jh1PEGH!{yfp*f3cslU z1z==l9Ef_%SP0RSdH+ZdR4 zatZ`Yt>eG5`oFvl7w34!0YEoDZE}K)t)q2VLrqyh_TYiN!oW%>J23zLvH-Bv0XsN( z_)UEOue&bacuo2{IzX*0PMa7T=pWJ5K72@BRasF^M(V&m5g`aK7yAw-(h3lH0Ro7B zff2~U#x1aGcjmVW{*NE(oB()p(-Q{A^t3fK0Bm_#X(IC%wkU;017etjGr2L<3y8R_e39|EK<2Vn2tCnmCIH-OH?v6H+I|8@C?H2>*jK!)c} zD6j48Y%ES09Rmitf{f(;y`+W<@bPk!w?%=ysO`Vrh#@b3kTn3zBmYxY|Cb}7ww9(R z086VPPttcl>byK$U=DWD_B?r0_SgGBWPFkau(EUWB>aTd6N?<6R%Ry+^Z-jMB@XC2 z9}lVFJILt?-?9^aS^xej0Kf)t$@l)>3AA|FTAP~~9yzQk55Nlo`VRCtIWGYK|J6?D z*L^_JZaXjpNGiw$_WD0t!H>SSq|QI8t)_5rAIa7L`u;U9gPfG`H9h6qHsmioz`?z{ z=(`25@P}&#{r)yK7N&rg*HDz+F9KLvcJl5gDJA1eVhVZd;cMf`B!1}uFxdDzcVKXD z&nN%Eg7wD`YXF|4d&;DhRuVk(*6uH``?{5i0Id1_$Nt;59$;nX;zfK%58%-6lUyC` z&pjLYjlm!c-gC9G0?giVJxygf3Bbz(yYS>qKXN_{&~P&Lf1rO^fd%FvBrC`*nEzKj zu=(aP+|2>-Z?;x$S*u^}Uh1!Ze#Ong(!$IHFngMS?(N$H;U+EhlXhFkef}rlq%PR% zf$e}B;Nm(y`4uJ;Yj?eQGpWdbfI9i#VHa9mrb;9tNj;4yjL2=<- zz;4qQL%-GMzi&9X`@iCojQ|G^uhU;FM||{fKI3Rda;~RLP8gnca<#WSX?*gOsj2BH zAj;4`qOCz5_`7()-`4qm-{)VEzx@9z73AZ0nfY%L*nB|p?PTX_cwGP3Q9Zq*`o|3n zjf{*84UUuiothFU$k@e4g5O$YC&B**;C~)&xq) zxOT0>hqbhIbaeHO966##^7LxT^3oEdAOrj@$N=E~6Yh7%TSj0TJzxcScL`toFAHM% z_9XjC(lTvLbyXFhsH&-JXdF6pNJCvsMNwW_Qd|@WGJXu6tb$*yAitnU;Gf3B!@hPT zc#@WpT)V7{jEt*V5b$>ZvT>S@ExrO#IYp+k{{yWh6o9p z6#lUYaMx{t(89#<&@zuZ4Drj-oH;&M0k&|u!xALnAqOEVxl5@gaA*^P1@@J z8f1K5>$e2()enLB1VzLTNFTC~zMYlzqoaA1hpLv1w7A`JsZXO<9US1wjt_vyQ2WUKC^|m6s?}q=@1Ef%d3(!GvDLExI zO`Rk928M=Ta$?ZICZ;C=)dS!qfUQ(Mpsj4=!p_dY!NJbXMhY{?SziD;IlTS>c#;YN zJpjaFOgl&t5Z)&tEw8Musik{F?^4g;MEBd0NMm3+L7v1(0`R~X19JVCfx^PVN-8YB z&cOV=pZy}|KmTv52S@}+o^aP5vHg-V@=B`enuoOwO)O99A3b^;m`7=mR1Yw4fPE!p zcWo6$QUTI&Nm;NYNB<@1@6WOR@X8kz*bdCd*}%L4yM+M;2W91z028Be=&-hqE@1Eu zsVRMfC*?VAB_eOzM*cv`MkYI3va9_=jQ@6$6bS$mLOPNJc5-s_K>!B(4oDu9kyB7o z2F#qgx|*so5L0iN_pO8;ptav}vbWNpe>CCoSB$<8pxs8#$g+bS%)=)jC?qVpPn^Vo zY~}zHCk4dRKwL-4CjyenfY$!10LhkL!;D|m`U4+-AwUuVJ0}+}KV;YLJtCre#rI1{ z0tJ{!k<2@33QK|~2UR3gav|rz166#t(fK{IuLS4-5iqmvWaHrE;^E_m2=3k^OtNxf zz)XrXhx`gpYAxX3$iQST|9=7}(b*zE8i(5%7@1he6u{g72LZsy?G^-bxp==ObCE2} zH&7BVsr||r-1aondjT~U(NXcwpX3!+~AFTf`Z{H$7 zV(^s$^A-mVPEJmatrX`kQ`mn1okZ^|gDnbMVjvs29e{lW4D4^Guz%U0AHM-0K22Rk^BC~tNzX>wio~uzHlJDdO>>cg7oGE$+vB(-gj2^w{9ab zAPuaq6i8N%{GP>^cN~DLf2-afc!*5l7Y^hXU`W>P?|GLW0RDFC76TFmk{rGi;DB`g z+fD!Uhi_sa(Etkh_@4&-P5oO8e)aEfuKw>1NDTgWX#XPpD}n#NOTf)hN(n?MmwnmT zP>+e8haPAz)6pZ^CxNd=0X|ZKw$TFrEyF&pgFw{WZdzK#9$I=@KAt|lCTHy(&*+`; zKI7}=cv9~$2&5PvZ))SpXUeW#>gOV~r^Uom_M-n|4JKixe9Ntd7DbuTjeABq#$g9Pr?FQfi_h;8TVd%_e_@^kDu2XQEvc+7Aqkzyg+^nD;j$a)e)4+A!9&%OA+LuWyQ}=v!{gnY$M%nOT%s{Gale$pn%Go>)n&vVeBK4UYv4ZfbZY#* zLt4PV-4|!k_M4erZ(T29ysENWZ)d7Uq3x~l;&Q^1!zZfspI*z&-m>wNKECK*eU~b_d6*8muPsy<~GjulF4D)Uj1n=-Mc| zB6IRln$CdItJ1r6M45fZtxdXf_A?hn3%%cqr?Dv~)IwiaPw!VaQ8dDx{>H2Y2gS`0 zq*uiW?hl3#V`huRzIB#Or7w?H&3c;q1)1`A(T;L1wB4?`T*Hq!w>W0Tq8om*;r3+y zvW=>Rj|8)BP72MsJMJ{MVZ3(Ea^!N%tNrDoaIuZr@@9A=ycymCe``aTr1Cy}v|?D| zEW{65yXubNu|$P(26Gj=OyBg#IVLFN$sxe$`%ZrNguV2VDC0KA!zX$oZsyM5IWr|Q z)cBblArskn>k5g}6qoTYD3=X3fyK+|v)A|XpY}0Otk7h;vPaA5%60iuYp=~P_jcbf z8b9gAYNBGo_T}DDedT&L`e4PkwH99E4L^bSQhzFg0@o>9;34@Z`(y zyP5iV>e6Gb==$Rct|l>`yexTX(1Xi~%W2Z3ENoU1dUx=Ql@%I`vv`S-gjb-OeCH$T zguJ<08|emV6fsesUiQ7^PdXCDF!XV4b``lU4GZ7+INe|Emos_be4&C$IueEU?@~WR zdExwDM%3L5a_73<|!D(?7;Rko0=pE_dU*n+anG&aY@0Z9E&H5;D$sZ^enn$ zAe%0sPiu60lauQClEu8I`RvE?)viB1b@J%#RyK~?2VHUscl)A070%p?nYhLM@1K7q z@c*6!UKdNB9Ui22(Up-oYbJdbsZIwSyy0n>Ekh+kHMyH=&0|;v+>oi6VL>|q5BGRF z`F5x)tf{l9lInJZ1sA0LeK^!RWbl2qpT*?xkpGfmZWCiahWnNdCIhK0Q+b222_GoD z*X2lROq9&=Rew2|v)R&84Abe2v#&xexo@paxt&>R`E)zxf=U|74emVjQAR3m=1I0c zkE%WGgj(=B>I|)wV3=YI*eg106Sy{qLq<=+>DJoSV3IX2XWk698eqZ)`|@U#aWYgc zHzP2o7Fzc2?P$AwX2^R05%eByA-o#yes`_Iw&nA>k%kOa=$WB#$*53ad;0XFv@IjH zDhv>pYdNJ;W2*A~7%q%WS#`%sQ>7-Y^V>T?@6uZ;(<|!cUu~$QXJQ(F6jcFiFb2FL(A%E=rg;APfPtHe2g2iA|9d8{+M>W35ijQ6ry1e80 zIh5^lCf?FCrbW$_^5K)mmzpYjo$NRp3mPyR6XUYzw7$ZC(vZ#F+DUQ{(8kBkWXLu`8Rh=6fSb^x+JFTJnRuf*_vDAIbqz}0gtj-{Khf7KV+ zZ{-J>1R5lkQ56@Ma#%e+bUe`W&W#qEV>4=_tA_HW7}61wzgT> z409ezI5YI1W-)fYSbsu-+CZM|(!r6@Pbzr?x7sk3in@&!KpUSp z4|%f}nhaFR76s87`sS{64hR>=Wj1>zm)h8R<_~+xcaNcUKBc=#)fEO?2M3q&ryJlj z5^_iDYf*Dk+6q`%&+0BfA4D4~H;l1{*U!^w(@fJr`1C^5b^U4fxn&q#W+#fLDCkHjRC-KG1cim2vh)gv~`?NgZH#?_z?R z(SUh}5~oeYBZm5v22cCVJ}yYM1-(XZg~s|hpsS1WpKBG~0x zL|=(Fe#$esx3*w93F%=l*(e13MnMZ|bG6uqJ$z%NCa#rz?u8|B?!6@PBY}9zyu+X% z$PVlAf}dVpJcpZjZzz%yqm%vI+3S+vIqDc@@tmOs%8Gn&|6|3xxQ9b0aY0;S%fgwE z8?C?>=s%i37D)^#Y`TZMztsBTe9Yj*Zr3c^6jV^f``V*8M?TRPK|2SkUo_Y90KZde z1$6eDnmiS#;-@Zi_7tDXz7{juuGE@5{@M24YLs>&QZ77uMJb(Ks(H09 zQEjz{1rm-O_EF!&j{0;74q3_#)M*e27)c~{Bu*pJy^NUx%DxfD0G9Y*!wk~Ff26qU zr2ZI(mqQ@2h{r22!c^^dG@g`1QVj^itvJ$1vMYv_nVu`NmiH$8Tw7}=!Lnx5?$E@& zmw1b}qBKb2d+6rU%o5Hv2?2+34Yg84K-8yTqQk?K;bo!2R}YCS^a}DLwGQ}jt4&yO zRb7Z5O|Vh!!}h^E&Uq#y`3L3Gv8X+M)*-VrKJ9g=+PKYgV{Z&I(KFdIO}M7Y1Kd5u zulw)If`blF75kM3PJ50*Vv0pn+Sw1hN@1uljbF`tx=37T5X~ff8h@Vx&8ez<=w6*V zr-j&M0wA#)}O3dW;x7myz{hYQ?m5n?%u#Cv1v`xo{N5H^;EJJ<8+ zLklka^SUjk-$EUOfaFORur+` zkr@u%3^!RQ9a&sFQL|F_j(Hyocy96Cpwhw&)uy`Spo`9$H*JYg9CFAKVL4<&&9xk) zvYxLn=GEkqbn}IgOpl&3G_=nXuuAG>2HtGw*UL7WhJAw)i1bX{Bd&L-Xrue3>bw zySRH!84FyU%S`hUSW}+6hN{(&$bY|9GWO`uJ&!jj47Pa_YZ_1O!}C+;f-*xsKk-r9 zSng-&nT-`rNac>5~t#-$AG@#eZHw~6Hh3~QH7)U94 zzd6@)xmj)i4y#MLJj*jUfiMVf7g%5%qcUWH@yt)pB^sh5n{YN(`*m%Vdag8+`(YjGv-|i zc~Io06p>jl6?t#k153ESXt61^CkKv{Ke1(EZ^?fhFV|N$GA!1 z$%Tc<*oJ%KMk6^?HKK)yGME7*r}1_gsBHybJBVkWO79Kf1EF z{YA8@_sfIwsb0PwWd=n-y-dhHRf$|t!9WKpAE;$I<1O0#_NEs4C0h_wZEZMh{BI6c!cQaMq&%mYL$pU4yoWqM?iK7bMNtLSOLQ!^OKh z3{otpteJV_+DonrC+n8j#hVo5K7xIO=@(bC=#D+(hVV1irwD`&MkB2aR+d~c;j3YC zi)J&di-B+Tc)TyG+E!%dRn-;Pu5>>m#(duO92BZMEl^YSfa=ine$|S2Dl-;bI`tuC zR>iSlNaN7unHRB1k@_5UzK@l{T;5u7G1D+yKM+G1LzSt5b&P;|gq?cD;yy3K0{Qq! z5Hwc6{9f>07qjBv!G)PfLh#tNQA-~lH%=-3rO}8B5+QjnRummXm*tRY%iU9bvlqMJ zvs!V-9ikkwFsly(RMI?koSU6t{Als+fLbM9xP}lS&xJO@n{YceRGexr2(Mdc5YFbq zmfo6F?2-H7YKi5-F}cS4)5#&Z3*w!M@aOZmcdZ<)DilM$ZLu2*OR;_8K1}AA&T5&a z1}E1?F$Upp-Kw!k!jX9qp)0m@=VLapbs7?{Q*&FH_rLIM$qW*}25x&Cv^!xi;=`?H z!xBhJyAFz76*w)1>jn>MB&`iXl&)XOs&kc`1!1c9o~{@{pWQq&JnqNJ5!VU{vs^g$ zRx~CiB2gbjNRF+d7_M5vm--S*Q}H+Id>9A;Q7rrkLh*0q{B|u)CJa7VETP#o&CH4$ z+O2xnlig#+JlQHMK*)oTB%h}+o!=1u>Kwn7$1-q~)wd{r+p7%j;jJhHw zN>ULPm~pdh!ZUs}o0jifwyQsd;{#$^)d)|!koyNE-{(f2hw{Q)cA@F9Gtr{-d_9Em zi(0*%vF}wBlt9(3mHFOts{Ld|*J*P+!|-ObB)TW(X{cG=$7W8KENn;`2syL1pYDMp0> zo%ONKVUK`k^$H__0IUX8KQp7ASavvS-gNXC4f<38!CCHF(9gj9dITMu#Wl@zb)-}2;}P1~kV9=dzbD zHTS0Dgl`Fy5^5BX{%zB@3wH4IP5JAwvS{?m9KU&BeL&ulmivvvvq$07g%!r~0+IrX zpIqi}`2m-3ek8a3WTVV11bu^$|K6J!WVoAIGZEYjwYJU;Q?)vWZHW}Wn7QxDp8itg+$Wg^-P;ko#vm&_RFcf?!>i!d=rOzmkd4mS1?*u!$V zSpLnPhYH9Kx&snyR{R|^(xY$Qr|K2q8Ol4l#tzTOcl*CX)mkMVcc-_Zz2g3)0W4SBM%F0KOCx(J>sK>R#poMt%SI{TXt0(1J zL(GUz#v|cw{cdFs3uF((8&feDj%s%h8s|?GtRB0aE_E45dSEed?9%+i?zjV~s zN|dlLK;_ew;q7wUVq?nWa*kqrysJ4?U?Z%O^|npZRjU_UQUujKVy<7Q=Fpg}h4l6u zNw^Qz@I2jtSne>=fWKPx#bqU`hir}}YJ`_hK(6znl75l?WwAhB6Vc8MFA+57Yc z(@U%ms#;)^?%P}%SenCgx#J$9ZLuEqXjQ_Kr7=lPbumKPef$hP9R-59mxV%U+nCID zhbg)p;DrgDrbk`p=F889`VBZ`&Ez!1aBfJn8bS5sH%pm6y&lw#+mzT$Eze6+DmhrR zu=-ZC6IEAty(ES#z_pPvO_V~c*HRh`-(G4x5zA2DJsR@fd=&AJ=MriWX#tGb1cj%I zNZYffqicA7WH(*8TZ-Wmfilw!k*p`CqvomiOQKUmo|MowwzglA3-DWikt9;al@Cev zOWl`gk^T1Cs6Lyakddd9K%ZO1NSyUZeQIv?1!n=CJLQW8DMo<}N@s_v{2MNXUQsK{ zh^u~|5cXn5$|N48BEn*pk$0oqBVZfsApy6s0Ujs5Z^sPB>Ajy262`dN6fn@}q4TQq zxC)5vD_|BQ=8DGgOmztq8tl3WxgN&MlbOPgSjPrWV+-nx=v6LZ)oIb3*v{(RRy_tu z_}oWz0_7BWy>HRW^@*=?^uoJYBsJToU$xPeoDg~Q)D-M*nZrtk=z2LRvQ6^_exKfrg%+A{L2_2o8YkMv$7SC{&V`)5jqf^^9j~3%D z_bf$j(_J~ybv3nu3bfEw9&Hn%mw)s9;EAe_#!vV1eJMcbCA+Bz*UM?`x8V6{4=M7PAy!Ki{C1kC6R49Bv^G9TmE z0@CwR`gv(8Zn@z&4&iJoo@=_tWcyCRdqr@dQIiF#Z$QxO{Hn zs7G@AQ4KxrTXg3OkmjRWMdDkcbzx!-*hI{TrhivD?8m>r7%`*96yTN zH{Z&^q-w=9<}0@`&JZR`ov8 zm4TrzGeekq}aTE|PtsD)nh3)5dJfanH0qNT2ScMcj!Tx0hq<8L~be z`1h>5!Advu<=e2s3OH?}FeS$-gv4k|!TTsc_R%}=*@#4PKR?{lj)211UpWM6UVVSDMb5cGn`u4>63|*=iA7)v8G_|V(&pE8;sE2kE8w+Bw1o)BMmj?3j z2h@8%f;0y&4)yg9j!3@v&{~F;*>p4uTVjKU%Z4-E@H#*7!5-N~m^gbV(O{RihmhJT=Cg!%o|BfB^24mf@QJ4|NXr-2xlogS zG1!3S-C4p@7&8SkwTAd&5Cb8k63Wjt-(g^|e>q%I@|acl73Hor$Yj&(NbYnsygNP@ z+sZR9J9FiP#&n_d=dxm%qr|9Va^hP|L*OaludBeImHz4|+B_Z|g?o z;LsL~?%So(jeSG;;IflUGrDr+GZ*1^h{LjhH0nC)1~s3oO+HNJ$i=9)J1N(8-)a9~ zXApIuS(obL<1%H#EGLbk;lWEIDJ+rkJu#zwcf1L0j(UEhEFU`ga4VXc2b0*KgaQ}! zyi$bfo3m?)wWp)aU%G_zgJ!Lmb82Yo6|Y=+h<6bWJ)3(dkERc{d}TJ-OMZc=XmNxk z9um?^mHHNP#Bc#ON;eSh*s30;dykpIh`r2w!Fqfp_x{GCFr{sT`wqC`*iooHFh?qD zr5|hMERhKjvPvtRx;Gi#`er6ZzA?Y8%^@xx?*Aq~FK)2TRb{jEcsB%>uIDmV*HYn| z1g*-pcWj=2Fi=A>S?NZb*db5MYlA>GpLq55mo0tIHkk?Il$j%Inict{cF;+J_Y+yO z>2V#V%PEiYlVA+iY{o+!)$FjV4w=s9k@B)s(xXSHVr_2ah~#LxRi?%aYk4~yk!cSr z)5(jtT$7sEqssS zz)ILlM9g_vCj$%hoZd^4OBQ`9p{wl?1AO#Nz3tbBXHy2S@7Q=VdYFp*Ha;}GYeW0y zomjondRiUgV05Ef@3V1XnYc0b;e93l3hQv_zFrr1BC*tZQ%^Ho@@2Tmc2D>NZto5u zw(!h+tFm6ZX@7j$#B7wyApA^uywRh?6G%)`sI^dj{@RTh<E->d-O6~oIwiteptgb;c zOFZqs5zh&N(*V5vXp@`oqR{DzUe4r4vDvrZxm~i!WLX_ejK^N2YCD=^p(kKi1*Qre zxhhVjv6;36@1WJQD3Q+W;Zl19V*b+Jmh!pEja@I?`b za+7XXQlvtgLW5^=;)Z}vv15k$;Y7o(vt$hm9LwUyH}W6x+3cP3Tq?8(^__R8ouC+M zd4SYlFIDwSwi(TB!%q48u?T&@!Y4QNO)sHxDDlo53qemocq3T88^QDz9I{!JszqNg z!gr+YbUg<`t3axKtZ7k}P0@EMn)ceG9V$+3J?+}Etof>PD$S6hM;=!bJzf1}As+HO zQ?$-MVp9vhf)%23&?_4Y^}NhtD6`BQC5QB-8H>+TJ%b`#rScS!@DM7iKBBvSF%veD zqJ~dS>A63{^LDmI)aC=6PyW+ILkVT4g`IlLAVQ-CtJy5ZE5p7yBdn4(G!+?*R<1uW zBIpBYDBTd4uLASg>|Ix(Nxad2)Ra$!93U9*Ot_!}!#L3hF(Fs!qV z!C&>wjITU1cM%9&=S=h0QDWF*9FzH{>?=;3E#x8UWPUPxEGNyRLt%D{DmG{$7P*`G z3Ojrm6ha^FRKIt9*>2`TyT2@6*8i3#^PY*Mn1UATENPaZzT}B(5zdvDYpio3WFO?r zU1o|m3rkH25|%ykV$ohZF$^-xl`f&{ue`(*7dclyjEO_}?VjhOoD$X;VBy_Wa!{0^ zenqVyGZG?zFA-RsGTWtO*Q-}7SRw0I(WJRteH&ZfE_V%4;akZjbo6EXFH zyy?2?k;kDi0%guZs<4!Br=Z@;{qkk``CjrOXh!L?1q;b(4p}_`1qnZhA#AyxvT` zppyV^Qe=mn?2SQJStV8J<#t6`u~o*ftGdaB=8Qdk%erA998VXQZ{hEFCZ_njA*4a% zA3oeM$-n)aOGYF;uiM!dbot}wjUrVmV6zR+Mg8%$ernb4jBIasqz+!*m75Bs@w4}FZ5xuQNo-|49hF;MC&{)kz2i zM(Mjvsu6VKqz?nqtS5#VVY^@(5ky;NFcz))`lye*{wpptlPo5oA-5su_D&_9B)ut& z_vjJW`#q~{qHSA=jJ_fuaDPcLQ2K+-I%OP(bQu8 zZJx_u`TA=NcLUVkj8Hi20QcE)Ox}1wt0z$@A6%j7>B17`bT=v6qbE{Of(kl2TcSSJ zQUe1TfD$z01JAC$d#q4=ma4j`5 zUg=OVLcaP`$R_0N!=<&5i|fax*Vl)QVmD(hK*5UCg!@i|2R4D9E!-A%y0TdBs9gpx zcGo>QCBAgXr%OU?s!O>ll9-_+)QUpGU6(zZNkv}#0}0)$kj>P@1?54ryqPN)uX9&k z`oVQcP9j-UnWawsW>G`lVwWr7ia-DS%Dy2~&f(9cIHX0*%G|!s`KBF6+h*c+R8#gM zm0$Dmdj=+LicR)f_s~MkR0sTC#=dDAU!(}1h*iCvvp6CiSnsMmcsizIS5LR+R22`6 z6uMcD&^5(VK-=)B+&R-UN7MR~d9BP)C_&QvbeM8n6=x2@^q%{UEuJx?Y~vZhdh zlvzy>-IRzjX!TW(W_2}R|7I3AUh!JtUyDq6e?;?zWo*4nt zpuyOc^YMwT$C5Zd1LDEUBTUMV0-OD^&5hhq?tL^M*X+V=ENTJN$n*gIFNIMAAe*IQ z&x-K1P7Xo%La$oIR)(8mlyRe+SUsd(>qV~Vfy=n_o~E1%T&KY&RR+PKd&|ueafF_W zY&Y{6wHRFDhLuvBs#hZ`=i)^2;SdOeAUNN8QbcjC83f54~vvdOP?}i)_1@$%u|ZEKHwW(P0?WP5+

gILN- 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%0DNpmL<0FaN`&ZnI#@qBw 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 diff --git a/packaging/macos/MRView.app/Contents/Resources/mrview_doc.icns b/packaging/macos/MRView.app/Contents/Resources/mrview_doc.icns deleted file mode 100644 index 630beba173029de8540fd848adaa7a341f383c26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 177431 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 !WtR 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 - - - - CFBundleInfoDictionaryVersion 6.0 - CFBundleDevelopmentRegion en-UK - CFBundleName SHView - CFBundleDisplayName SHView - CFBundleExecutable shview - CFBundleIconFile mrview.icns - CFBundleIdentifier org.mrtrix.shview - CFBundlePackageType APPL - CFBundleShortVersionString 3.0 - CFBundleVersion 3.0 - NSHumanReadableCopyright Copyright (c) 2008-2023 the MRtrix3 contributors - LSMinimumSystemVersion 10.14.0 - LSBackgroundOnly 0 - NSHighResolutionCapable - - diff --git a/packaging/macos/SHView.app/Contents/Resources b/packaging/macos/SHView.app/Contents/Resources deleted file mode 120000 index 4b771a36eb..0000000000 --- a/packaging/macos/SHView.app/Contents/Resources +++ /dev/null @@ -1 +0,0 @@ -../../MRView.app/Contents/Resources \ No newline at end of file diff --git a/packaging/macos/SHView.app/Contents/lib b/packaging/macos/SHView.app/Contents/lib deleted file mode 120000 index a5bc7439f9..0000000000 --- a/packaging/macos/SHView.app/Contents/lib +++ /dev/null @@ -1 +0,0 @@ -../../../lib \ No newline at end of file diff --git a/packaging/macos/SHView.app/Contents/plugins b/packaging/macos/SHView.app/Contents/plugins deleted file mode 120000 index e18ba3b9ff..0000000000 --- a/packaging/macos/SHView.app/Contents/plugins +++ /dev/null @@ -1 +0,0 @@ -../../../bin/plugins \ No newline at end of file diff --git a/packaging/macos/build b/packaging/macos/build index ff10f88a3a..69d458605a 100755 --- a/packaging/macos/build +++ b/packaging/macos/build @@ -171,18 +171,6 @@ if [ "${OS}" = "macos" ]; then fi rm -rf ${PREFIX}_dep -if [ "${OS}" = "macos" ]; then - cp -R ${PLD}/MRView.app ${PREFIX}/bin - mkdir -p ${PREFIX}/bin/MRView.app/Contents/MacOS/ - mv ${PREFIX}/bin/mrview ${PREFIX}/bin/MRView.app/Contents/MacOS/ - cp ${PLD}/mrview ${PREFIX}/bin - - cp -R ${PLD}/SHView.app ${PREFIX}/bin - mkdir -p ${PREFIX}/bin/SHView.app/Contents/MacOS/ - mv ${PREFIX}/bin/shview ${PREFIX}/bin/SHView.app/Contents/MacOS/ - cp ${PLD}/shview ${PREFIX}/bin -fi - cd ${PREFIX}/.. tar cfJ mrtrix3-${OS}-${TAGNAME}.tar.xz mrtrix3 rm -rf ${PREFIX} diff --git a/packaging/macos/mrview b/packaging/macos/mrview deleted file mode 100755 index d4a08e287b..0000000000 --- a/packaging/macos/mrview +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -f="${BASH_SOURCE}" -while [[ -L "${f}" ]]; do - f2="$(readlink "${f}")" - if [[ "${f2}" = /* ]]; then f="${f2}"; else f="$(dirname "${f}")/${f2}"; fi -done -f=$(dirname "${f}") -"${f}"/MRView.app/Contents/MacOS/mrview "$@" diff --git a/packaging/macos/shview b/packaging/macos/shview deleted file mode 100755 index 3ec290edd1..0000000000 --- a/packaging/macos/shview +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -f="${BASH_SOURCE}" -while [[ -L "${f}" ]]; do - f2="$(readlink "${f}")" - if [[ "${f2}" = /* ]]; then f="${f2}"; else f="$(dirname "${f}")/${f2}"; fi -done -f=$(dirname "${f}") -"${f}"/SHView.app/Contents/MacOS/shview "$@" From 2f8df8d5f57ec06c5b341621ab48eaaafb8c0d43 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Wed, 19 Jun 2024 12:25:25 +0200 Subject: [PATCH 104/182] Remove MRTRIX_USE_QT6 as it is no longer supported --- packaging/macos/build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/macos/build b/packaging/macos/build index 69d458605a..5d28f321c4 100755 --- a/packaging/macos/build +++ b/packaging/macos/build @@ -140,7 +140,7 @@ git clone https://github.com/MRtrix3/mrtrix3.git mrtrix3-src -b ${TAGNAME} cd ${PREFIX}-src sed -i.bak -e '90,101d' cmake/FindFFTW.cmake MRTRIX_VERSION=$(git describe --abbrev=8 | tr '-' '_') -cmake -B build -G Ninja -DMRTRIX_USE_QT6=ON -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/X11;/usr/X11R6" -DFFTW_USE_STATIC_LIBS=ON -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCHS} -DPNG_PNG_INCLUDE_DIR=${PREFIX}/include/QtPng -DPNG_LIBRARY=${PREFIX}/lib/libQt6BundledLibpng.a -DCMAKE_INSTALL_PREFIX=${PREFIX} +cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/X11;/usr/X11R6" -DFFTW_USE_STATIC_LIBS=ON -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCHS} -DPNG_PNG_INCLUDE_DIR=${PREFIX}/include/QtPng -DPNG_LIBRARY=${PREFIX}/lib/libQt6BundledLibpng.a -DCMAKE_INSTALL_PREFIX=${PREFIX} cmake --build build cd .. MRTRIX_SECONDS=${SECONDS} From 73e46870f2b57c9ddaad95e1bca2bf5df17392ff Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Wed, 19 Jun 2024 13:01:12 +0200 Subject: [PATCH 105/182] Make sure app bundles can find the bundled qt6 plugins --- packaging/macos/build | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packaging/macos/build b/packaging/macos/build index 5d28f321c4..4f0515e9f3 100755 --- a/packaging/macos/build +++ b/packaging/macos/build @@ -171,6 +171,11 @@ if [ "${OS}" = "macos" ]; then fi rm -rf ${PREFIX}_dep +cd ${PREFIX}/bin/mrview.app/Contents +ln -s ../../../bin/plugins +cd ${PREFIX}/bin/shview.app/Contents +ln -s ../../../bin/plugins + cd ${PREFIX}/.. tar cfJ mrtrix3-${OS}-${TAGNAME}.tar.xz mrtrix3 rm -rf ${PREFIX} From 96ed3f17acdc60ccea40f7dd3b04f65ea5912140 Mon Sep 17 00:00:00 2001 From: J-Donald Tournier Date: Wed, 19 Jun 2024 12:06:54 +0100 Subject: [PATCH 106/182] remove TIFF from build system following removal from code --- .github/workflows/checks.yml | 1 - core/CMakeLists.txt | 13 +------------ packaging/mingw/PKGBUILD | 2 -- packaging/package-linux-tarball.sh | 3 +-- 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4a4487d3ce..89f85fafcd 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -211,7 +211,6 @@ jobs: ${{env.MINGW_PACKAGE_PREFIX}}-eigen3 ${{env.MINGW_PACKAGE_PREFIX}}-fftw ${{env.MINGW_PACKAGE_PREFIX}}-gcc - ${{env.MINGW_PACKAGE_PREFIX}}-libtiff ${{env.MINGW_PACKAGE_PREFIX}}-ninja ${{env.MINGW_PACKAGE_PREFIX}}-pkg-config ${{env.MINGW_PACKAGE_PREFIX}}-qt6-base diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 395c089319..7cc54e9388 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -8,17 +8,13 @@ find_package(FFTW REQUIRED) find_package(Git QUIET) find_package(Threads REQUIRED) find_package(PNG QUIET) -find_package(TIFF QUIET) -# The find modules for Eigen3, PNG and TIFF don't log the location +# The find modules for Eigen3, and PNG don't log the location # of the libraries, so we do it manually here message(STATUS "Found Eigen3: ${EIGEN3_INCLUDE_DIR}") if(PNG_FOUND) message(STATUS "Found PNG: ${PNG_LIBRARIES}") endif() -if(TIFF_FOUND) - message(STATUS "Found TIFF: ${TIFF_LIBRARIES}") -endif() add_library(mrtrix-core SHARED ${CORE_SRCS}) add_library(mrtrix::core ALIAS mrtrix-core) @@ -86,13 +82,6 @@ else() message(WARNING "libpng not found, disabling PNG support") endif() -if(TIFF_FOUND) - target_compile_definitions(mrtrix-core PUBLIC MRTRIX_TIFF_SUPPORT) - target_link_libraries(mrtrix-core PUBLIC TIFF::TIFF) -else() - message(WARNING "libtiff not found, disabling TIFF support") -endif() - target_link_libraries(mrtrix-core PUBLIC Eigen3::Eigen ZLIB::ZLIB diff --git a/packaging/mingw/PKGBUILD b/packaging/mingw/PKGBUILD index d1c8ddc43a..0753f7cbc5 100644 --- a/packaging/mingw/PKGBUILD +++ b/packaging/mingw/PKGBUILD @@ -10,7 +10,6 @@ pkgdesc="Tools for the analysis of diffusion MRI data (mingw-w64)" depends=("python" "${MINGW_PACKAGE_PREFIX}-qt6-svg" "${MINGW_PACKAGE_PREFIX}-fftw" - "${MINGW_PACKAGE_PREFIX}-libtiff" "${MINGW_PACKAGE_PREFIX}-zlib") makedepends=("git" "python" @@ -22,7 +21,6 @@ makedepends=("git" "${MINGW_PACKAGE_PREFIX}-ninja" "${MINGW_PACKAGE_PREFIX}-fftw" "${MINGW_PACKAGE_PREFIX}-gcc" - "${MINGW_PACKAGE_PREFIX}-libtiff" "${MINGW_PACKAGE_PREFIX}-qt6-svg" "${MINGW_PACKAGE_PREFIX}-qt6-base" "${MINGW_PACKAGE_PREFIX}-zlib") diff --git a/packaging/package-linux-tarball.sh b/packaging/package-linux-tarball.sh index cc7f3cea22..66600015e2 100755 --- a/packaging/package-linux-tarball.sh +++ b/packaging/package-linux-tarball.sh @@ -43,7 +43,6 @@ sudo apt install cmake \ libeigen3-dev \ zlib1g-dev \ libfftw3-dev \ - libtiff5-dev \ libpng-dev if ! command -v clang++-17 &> /dev/null @@ -88,4 +87,4 @@ rm -rf appdir/usr/translations # Remove translations since we don't need them cp $source_dir/install_mime_types.sh appdir/usr cp $source_dir/set_path appdir/usr tar -czf mrtrix.tar.gz -C appdir/usr . -cp mrtrix.tar.gz $running_dir \ No newline at end of file +cp mrtrix.tar.gz $running_dir From 94cecc2a00af98f83e288a5072fa62da0b7c0dab Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Wed, 19 Jun 2024 15:34:28 +0200 Subject: [PATCH 107/182] Bump up OSX_DEPLOYMENT_TARGET to 11.00 This means we are supporting macOS versions still supported by Apple + the most recent version no longer supported by Apple. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index de5ede25e1..d781ea7554 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) -set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "") +set(CMAKE_OSX_DEPLOYMENT_TARGET 11.00 CACHE STRING "") project(mrtrix3 LANGUAGES CXX VERSION 3.0.4) if(NOT CMAKE_GENERATOR STREQUAL "Ninja") From 291a7476eab53d7571f1a4b5f205ee643eca097c Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Wed, 12 Jun 2024 19:38:47 +0100 Subject: [PATCH 108/182] Cleanup cmake scripts for Python commands --- cmake/GenPythonCommandsLists.cmake | 16 +++----- cmake/MakePythonExecutable.cmake | 51 ++++++++++++++++---------- python/mrtrix3/commands/CMakeLists.txt | 4 -- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/cmake/GenPythonCommandsLists.cmake b/cmake/GenPythonCommandsLists.cmake index 93f3dcb1df..672a188961 100644 --- a/cmake/GenPythonCommandsLists.cmake +++ b/cmake/GenPythonCommandsLists.cmake @@ -11,24 +11,20 @@ file( set(MRTRIX_CPP_COMMAND_LIST "") foreach(CPP_COMMAND_FILE ${CPP_COMMAND_FILES}) get_filename_component(CPP_COMMAND_NAME ${CPP_COMMAND_FILE} NAME_WE) - if(MRTRIX_CPP_COMMAND_LIST STREQUAL "") - set(MRTRIX_CPP_COMMAND_LIST "\"${CPP_COMMAND_NAME}\"") - else() - set(MRTRIX_CPP_COMMAND_LIST "${MRTRIX_CPP_COMMAND_LIST},\n \"${CPP_COMMAND_NAME}\"") - endif() + list(APPEND MRTRIX_CPP_COMMAND_LIST "\"${CPP_COMMAND_NAME}\"") endforeach() set(MRTRIX_PYTHON_COMMAND_LIST "") foreach(PYTHON_ROOT_ENTRY ${PYTHON_ROOT_ENTRIES}) get_filename_component(PYTHON_COMMAND_NAME ${PYTHON_ROOT_ENTRY} NAME_WE) if(NOT ${PYTHON_COMMAND_NAME} STREQUAL "CMakeLists" AND NOT ${PYTHON_COMMAND_NAME} STREQUAL "__init__") - if(MRTRIX_PYTHON_COMMAND_LIST STREQUAL "") - set(MRTRIX_PYTHON_COMMAND_LIST "\"${PYTHON_COMMAND_NAME}\"") - else() - set(MRTRIX_PYTHON_COMMAND_LIST "${MRTRIX_PYTHON_COMMAND_LIST},\n \"${PYTHON_COMMAND_NAME}\"") - endif() + list(APPEND MRTRIX_PYTHON_COMMAND_LIST "\"${PYTHON_COMMAND_NAME}\"") endif() endforeach() + +string(REPLACE ";" "," MRTRIX_CPP_COMMAND_LIST "${MRTRIX_CPP_COMMAND_LIST}") +string(REPLACE ";" "," MRTRIX_PYTHON_COMMAND_LIST "${MRTRIX_PYTHON_COMMAND_LIST}") + message(VERBOSE "Completed GenPythonCommandsList() function") message(VERBOSE "Formatted list of MRtrix3 C++ commands: ${MRTRIX_CPP_COMMAND_LIST}") message(VERBOSE "Formatted list of MRtrix3 Python commands: ${MRTRIX_PYTHON_COMMAND_LIST}") diff --git a/cmake/MakePythonExecutable.cmake b/cmake/MakePythonExecutable.cmake index 38b55bcd84..850ff600ea 100644 --- a/cmake/MakePythonExecutable.cmake +++ b/cmake/MakePythonExecutable.cmake @@ -2,16 +2,20 @@ # a short Python executable that is used to run a Python command from the terminal # Receives name of the command as ${CMDNAME}; output build directory as ${BUILDDIR} set(BINPATH "${BUILDDIR}/temporary/python/${CMDNAME}") -file(WRITE ${BINPATH} "#!/usr/bin/python3\n") -file(APPEND ${BINPATH} "# -*- coding: utf-8 -*-\n") -file(APPEND ${BINPATH} "\n") -file(APPEND ${BINPATH} "import importlib\n") -file(APPEND ${BINPATH} "import os\n") -file(APPEND ${BINPATH} "import sys\n") -file(APPEND ${BINPATH} "\n") -file(APPEND ${BINPATH} "mrtrix_lib_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib'))\n") -file(APPEND ${BINPATH} "sys.path.insert(0, mrtrix_lib_path)\n") -file(APPEND ${BINPATH} "from mrtrix3.app import _execute\n") + +set(BINPATH_CONTENTS + "#!/usr/bin/python3\n" + "# -*- coding: utf-8 -*-\n" + "\n" + "import importlib\n" + "import os\n" + "import sys\n" + "\n" + "mrtrix_lib_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib'))\n" + "sys.path.insert(0, mrtrix_lib_path)\n" + "from mrtrix3.app import _execute\n" +) + # Three possible interfaces: # 1. Standalone file residing in commands/ # 2. File stored in location commands//.py, which will contain usage() and execute() functions @@ -19,22 +23,29 @@ file(APPEND ${BINPATH} "from mrtrix3.app import _execute\n") # TODO Port population_template to type 3; both for readability and to ensure that it works if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/__init__.py") if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/usage.py" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/execute.py") - file(APPEND ${BINPATH} "module_usage = importlib.import_module('.usage', 'mrtrix3.commands.${CMDNAME}')\n") - file(APPEND ${BINPATH} "module_execute = importlib.import_module('.execute', 'mrtrix3.commands.${CMDNAME}')\n") - file(APPEND ${BINPATH} "_execute(module_usage.usage, module_execute.execute)\n") + string(APPEND BINPATH_CONTENTS + "module_usage = importlib.import_module('.usage', 'mrtrix3.commands.${CMDNAME}')\n" + "module_execute = importlib.import_module('.execute', 'mrtrix3.commands.${CMDNAME}')\n" + "_execute(module_usage.usage, module_execute.execute)\n" + ) elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/${CMDNAME}.py") - file(APPEND ${BINPATH} "module = importlib.import_module('.${CMDNAME}', 'mrtrix3.commands.${CMDNAME}')\n") - file(APPEND ${BINPATH} "_execute(module.usage, module.execute)\n") + string(APPEND BINPATH_CONTENTS + "module = importlib.import_module('.${CMDNAME}', 'mrtrix3.commands.${CMDNAME}')\n" + "_execute(module.usage, module.execute)\n" + ) else() message(FATAL_ERROR "Malformed filesystem structure for Python command ${CMDNAME}") endif() elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}.py") - file(APPEND ${BINPATH} "module = importlib.import_module('.${CMDNAME}', 'mrtrix3.commands')\n") - file(APPEND ${BINPATH} "_execute(module.usage, module.execute)\n") + string(APPEND BINPATH_CONTENTS + "module = importlib.import_module('${CMDNAME}')\n" + "_execute(module.usage, module.execute)\n" + ) else() message(FATAL_ERROR "Malformed filesystem structure for Python command ${CMDNAME}") endif() -file(COPY ${BINPATH} DESTINATION ${BUILDDIR}/bin - FILE_PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ -) +file(WRITE ${OUTPUT_DIR}/${CMDNAME} ${BINPATH_CONTENTS}) +file(CHMOD ${OUTPUT_DIR}/${CMDNAME} FILE_PERMISSIONS + OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ +) diff --git a/python/mrtrix3/commands/CMakeLists.txt b/python/mrtrix3/commands/CMakeLists.txt index bbfcfcd0bb..e71c718e0c 100644 --- a/python/mrtrix3/commands/CMakeLists.txt +++ b/python/mrtrix3/commands/CMakeLists.txt @@ -70,7 +70,6 @@ endforeach() add_custom_target(MakePythonExecutables ALL) -file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/temporary/python) set(PYTHON_BIN_FILES "") foreach(CMDNAME ${PYTHON_COMMAND_LIST}) add_custom_command( @@ -81,9 +80,6 @@ foreach(CMDNAME ${PYTHON_COMMAND_LIST}) list(APPEND PYTHON_BIN_FILES ${PROJECT_BINARY_DIR}/bin/${CMDNAME}) endforeach() -set_target_properties(MakePythonExecutables - PROPERTIES ADDITIONAL_CLEAN_FILES "${PROJECT_BINARY_DIR}/temporary/python" -) # We need to generate a list of MRtrix3 commands: # function run.command() does different things if it is executing an MRtrix3 command vs. an external command, From 70fea8d497a59c1693df46a152df6216960fb1a5 Mon Sep 17 00:00:00 2001 From: Daljit Date: Fri, 14 Jun 2024 17:38:02 +0100 Subject: [PATCH 109/182] Add CMake targets for Python files This directs IDEs like Qt Creator to show the Python code as project files. --- python/mrtrix3/CMakeLists.txt | 4 ++++ python/mrtrix3/commands/CMakeLists.txt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/python/mrtrix3/CMakeLists.txt b/python/mrtrix3/CMakeLists.txt index c6f0613852..b11c6378bb 100644 --- a/python/mrtrix3/CMakeLists.txt +++ b/python/mrtrix3/CMakeLists.txt @@ -60,6 +60,10 @@ set_target_properties(MakePythonVersionFile PROPERTIES ADDITIONAL_CLEAN_FILES ${PYTHON_VERSION_FILE} ) +add_custom_target(PythonLibFiles + SOURCES ${PYTHON_LIB_FILES} +) + install(FILES ${PYTHON_LIB_FILES} PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ DESTINATION ${CMAKE_INSTALL_LIBDIR}/mrtrix3 diff --git a/python/mrtrix3/commands/CMakeLists.txt b/python/mrtrix3/commands/CMakeLists.txt index e71c718e0c..e7bb4a0faa 100644 --- a/python/mrtrix3/commands/CMakeLists.txt +++ b/python/mrtrix3/commands/CMakeLists.txt @@ -95,6 +95,10 @@ add_custom_command( WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} ) +add_custom_target(PythonCommands + SOURCES ${PYTHON_ALL_COMMANDS_FILES} +) + install(FILES ${PYTHON_BIN_FILES} PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION ${CMAKE_INSTALL_BINDIR} From 6f2948a39d04f21631e8cf6f2492039055d40b90 Mon Sep 17 00:00:00 2001 From: Daljit Date: Thu, 20 Jun 2024 16:45:07 +0100 Subject: [PATCH 110/182] Change/extend inputs for MakePythonExecutable - Instead of specifying a BUILDDIR, the caller must specify an OUTPUT_DIR directory where the output file will be written. - The script now additionally accepts a list of directories that will be prepended to the system path in the generated script. This shouldn't be necessary for most commands. --- cmake/MakePythonExecutable.cmake | 20 +++++++++++++++----- python/mrtrix3/commands/CMakeLists.txt | 3 +-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cmake/MakePythonExecutable.cmake b/cmake/MakePythonExecutable.cmake index 850ff600ea..2da9e69e72 100644 --- a/cmake/MakePythonExecutable.cmake +++ b/cmake/MakePythonExecutable.cmake @@ -1,7 +1,8 @@ -# Creates within the bin/ sub-directory of the project build directory -# a short Python executable that is used to run a Python command from the terminal -# Receives name of the command as ${CMDNAME}; output build directory as ${BUILDDIR} -set(BINPATH "${BUILDDIR}/temporary/python/${CMDNAME}") +# Creates a short Python executable that is used to run a Python command from the terminal. +# Inputs: +# - CMDNAME: Name of the command +# - OUTPUT_DIR: Directory in which to create the executable +# - EXTRA_PATH_DIRS: an optional list of directories to be prepended to the Python path (shouldn't be necessary for most commands). set(BINPATH_CONTENTS "#!/usr/bin/python3\n" @@ -13,9 +14,18 @@ set(BINPATH_CONTENTS "\n" "mrtrix_lib_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib'))\n" "sys.path.insert(0, mrtrix_lib_path)\n" - "from mrtrix3.app import _execute\n" ) +if(EXTRA_PATH_DIRS) + foreach(PATH_DIR ${EXTRA_PATH_DIRS}) + string(APPEND BINPATH_CONTENTS + "sys.path.insert(0, '${PATH_DIR}')\n" + ) + endforeach() +endif() + +string(APPEND BINPATH_CONTENTS "from mrtrix3.app import _execute\n") + # Three possible interfaces: # 1. Standalone file residing in commands/ # 2. File stored in location commands//.py, which will contain usage() and execute() functions diff --git a/python/mrtrix3/commands/CMakeLists.txt b/python/mrtrix3/commands/CMakeLists.txt index e7bb4a0faa..f8a37c0343 100644 --- a/python/mrtrix3/commands/CMakeLists.txt +++ b/python/mrtrix3/commands/CMakeLists.txt @@ -75,12 +75,11 @@ foreach(CMDNAME ${PYTHON_COMMAND_LIST}) add_custom_command( TARGET MakePythonExecutables WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMAND ${CMAKE_COMMAND} -DCMDNAME=${CMDNAME} -DBUILDDIR=${PROJECT_BINARY_DIR} -P ${PROJECT_SOURCE_DIR}/cmake/MakePythonExecutable.cmake + COMMAND ${CMAKE_COMMAND} -DCMDNAME=${CMDNAME} -DOUTPUT_DIR="${PROJECT_BINARY_DIR}/bin" -P ${PROJECT_SOURCE_DIR}/cmake/MakePythonExecutable.cmake ) list(APPEND PYTHON_BIN_FILES ${PROJECT_BINARY_DIR}/bin/${CMDNAME}) endforeach() - # We need to generate a list of MRtrix3 commands: # function run.command() does different things if it is executing an MRtrix3 command vs. an external command, # but unlike prior software versions we cannot simply interrogate the contents of the bin/ directory at runtime From 1f1fb1d6395034abd66a81bf777b50f42cee651d Mon Sep 17 00:00:00 2001 From: Daljit Date: Thu, 20 Jun 2024 16:59:29 +0100 Subject: [PATCH 111/182] Fix execution of unit tests - Executables for Python testing tools which are essentially MRtrix3 Python commands (e.g. they are defined by a usage and run function), are generated using the MakePythonExecutable script. - Additionally, some renaming of variables has been performed for consistency. --- testing/tools/CMakeLists.txt | 45 +++++++++++++------ ...testing_check_npy => testing_check_npy.py} | 0 .../{testing_gen_npy => testing_gen_npy.py} | 0 ...sting_python_cli => testing_python_cli.py} | 6 --- 4 files changed, 31 insertions(+), 20 deletions(-) rename testing/tools/{testing_check_npy => testing_check_npy.py} (100%) rename testing/tools/{testing_gen_npy => testing_gen_npy.py} (100%) rename testing/tools/{testing_python_cli => testing_python_cli.py} (98%) diff --git a/testing/tools/CMakeLists.txt b/testing/tools/CMakeLists.txt index 5a99c0f0bb..8b3113e43e 100644 --- a/testing/tools/CMakeLists.txt +++ b/testing/tools/CMakeLists.txt @@ -16,31 +16,48 @@ set(CPP_TOOLS_SRCS ) set(PYTHON_TOOLS_SRCS - testing_check_npy - testing_gen_npy - testing_python_cli + testing_check_npy.py + testing_gen_npy.py + testing_python_cli.py ) -add_custom_target(testing_tools) +add_custom_target(testing_tools ALL) -function(add_testing_cmd CMD_SRC) +function(add_cpp_tool TOOL_SRC) # Extract the filename without an extension (NAME_WE) - get_filename_component(CMD_NAME ${CMD} NAME_WE) - add_executable(${CMD_NAME} ${CMD}) - target_link_libraries(${CMD_NAME} PRIVATE + get_filename_component(TOOL_NAME ${TOOL_SRC} NAME_WE) + add_executable(${TOOL_NAME} ${TOOL_SRC}) + target_link_libraries(${TOOL_NAME} PRIVATE mrtrix::headless mrtrix::exec-version-lib mrtrix::tests-lib ) - set_target_properties(${CMD_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) - add_dependencies(testing_tools ${CMD_NAME}) + set_target_properties(${TOOL_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true) + add_dependencies(testing_tools ${TOOL_NAME}) endfunction() +function(add_python_tool TOOL_SRC IS_MRTRIX_CMD) + get_filename_component(TOOL_NAME ${TOOL_SRC} NAME_WE) + # If the tool behaves like a Python MRtrix3 command, then we need to generate its executable using + # the MakePythonExecutable script + if(IS_MRTRIX_CMD) + set(MRTRIX_PYTHON_LIB_PATH "${PROJECT_BINARY_DIR}/lib") + add_custom_command( + TARGET testing_tools PRE_BUILD + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND ${CMAKE_COMMAND} -DCMDNAME=${TOOL_NAME} -DOUTPUT_DIR="${CMAKE_CURRENT_BINARY_DIR}" -DEXTRA_PATH_DIRS="${MRTRIX_PYTHON_LIB_PATH}" -P ${PROJECT_SOURCE_DIR}/cmake/MakePythonExecutable.cmake + ) + # Install "module" file alongside the executable + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${TOOL_SRC} ${CMAKE_CURRENT_BINARY_DIR}/${TOOL_SRC} COPYONLY) + else() + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${TOOL_SRC} ${CMAKE_CURRENT_BINARY_DIR}/${TOOL_NAME} COPYONLY) + endif() +endfunction() foreach(CMD ${CPP_TOOLS_SRCS}) - add_testing_cmd(${CMD}) + add_cpp_tool(${CMD}) endforeach(CMD) -foreach(CMD ${PYTHON_TOOLS_SRCS}) - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${CMD} ${CMAKE_CURRENT_BINARY_DIR}/${CMD} COPYONLY) -endforeach(CMD) +add_python_tool(testing_check_npy.py FALSE) +add_python_tool(testing_gen_npy.py FALSE) +add_python_tool(testing_python_cli.py TRUE) diff --git a/testing/tools/testing_check_npy b/testing/tools/testing_check_npy.py similarity index 100% rename from testing/tools/testing_check_npy rename to testing/tools/testing_check_npy.py diff --git a/testing/tools/testing_gen_npy b/testing/tools/testing_gen_npy.py similarity index 100% rename from testing/tools/testing_gen_npy rename to testing/tools/testing_gen_npy.py diff --git a/testing/tools/testing_python_cli b/testing/tools/testing_python_cli.py similarity index 98% rename from testing/tools/testing_python_cli rename to testing/tools/testing_python_cli.py index f54e571dde..9b6245ee66 100755 --- a/testing/tools/testing_python_cli +++ b/testing/tools/testing_python_cli.py @@ -136,9 +136,3 @@ def execute(): #pylint: disable=unused-variable value = getattr(app.ARGS, key) if value is not None: app.console(f'{key}: {repr(value)}') - - - -# Execute the script -import mrtrix3 #pylint: disable=wrong-import-position -mrtrix3.execute() #pylint: disable=no-member From cee8d41025e6f7e9853eb28f868d15f0ed44b016 Mon Sep 17 00:00:00 2001 From: Daljit Date: Thu, 20 Jun 2024 17:22:11 +0100 Subject: [PATCH 112/182] Fix permissions with CMake < 3.19 --- cmake/MakePythonExecutable.cmake | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cmake/MakePythonExecutable.cmake b/cmake/MakePythonExecutable.cmake index 2da9e69e72..bed01475b7 100644 --- a/cmake/MakePythonExecutable.cmake +++ b/cmake/MakePythonExecutable.cmake @@ -55,7 +55,18 @@ else() message(FATAL_ERROR "Malformed filesystem structure for Python command ${CMDNAME}") endif() -file(WRITE ${OUTPUT_DIR}/${CMDNAME} ${BINPATH_CONTENTS}) -file(CHMOD ${OUTPUT_DIR}/${CMDNAME} FILE_PERMISSIONS - OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ -) +if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19) + file(WRITE ${OUTPUT_DIR}/${CMDNAME} ${BINPATH_CONTENTS}) + file(CHMOD ${OUTPUT_DIR}/${CMDNAME} FILE_PERMISSIONS + OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ + ) +else() + set(tmp_output_file "${CMAKE_CURRENT_BINARY_DIR}/tmp_${CMDNAME}") + file(WRITE ${tmp_output_file} ${BINPATH_CONTENTS}) + file(COPY ${tmp_output_file} DESTINATION ${OUTPUT_DIR} + FILE_PERMISSIONS + OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ + ) + file(RENAME ${OUTPUT_DIR}/tmp_${CMDNAME} ${OUTPUT_DIR}/${CMDNAME}) + file(REMOVE ${tmp_output_file}) +endif() From 3fce6484a071acd6ea703b05bdf9e21064153629 Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Fri, 21 Jun 2024 13:25:58 +1000 Subject: [PATCH 113/182] Fix Python commands, documentation generation, CLI test - Fix importlib call in cmake-generated Python executables for stand-alone Python files; this regressed in cleanup commit 291a7476eab53d7571f1a4b5f205ee643eca097c, which worked for the Python CLI test (as both executable and source file resided in the same directory) but broke execution of other Python commands. - Change handling of Python CLI test: executable will be placed in ${PROJECT_BINARY_DIR}/bin; source code will be placed in ${PROJECT_BUILD_DIR}/lib/mrtrix3/commands/. - Update docs/generate_user_docs.sh to reflect new handling of Python commands. - Auto-update of some Python command help pages to propagate prior changes. - Add per-test labels to unit tests. --- cmake/MakePythonExecutable.cmake | 15 ++------ docs/generate_user_docs.sh | 6 +-- docs/reference/commands/5ttgen.rst | 2 +- docs/reference/commands/dwi2mask.rst | 2 +- docs/reference/commands/dwi2response.rst | 2 +- docs/reference/commands/dwibiascorrect.rst | 2 +- docs/reference/commands/dwinormalise.rst | 2 +- testing/tools/CMakeLists.txt | 45 +++++++++++++--------- testing/unit_tests/CMakeLists.txt | 2 +- 9 files changed, 38 insertions(+), 40 deletions(-) diff --git a/cmake/MakePythonExecutable.cmake b/cmake/MakePythonExecutable.cmake index 9f7e4edfc2..b20cbbb878 100644 --- a/cmake/MakePythonExecutable.cmake +++ b/cmake/MakePythonExecutable.cmake @@ -2,7 +2,6 @@ # Inputs: # - CMDNAME: Name of the command # - OUTPUT_DIR: Directory in which to create the executable -# - EXTRA_PATH_DIRS: an optional list of directories to be prepended to the Python path (shouldn't be necessary for most commands). set(BINPATH_CONTENTS "#!/usr/bin/python3\n" @@ -14,18 +13,10 @@ set(BINPATH_CONTENTS "\n" "mrtrix_lib_path = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib'))\n" "sys.path.insert(0, mrtrix_lib_path)\n" + "from mrtrix3.app import _execute\n" + "\n" ) -if(EXTRA_PATH_DIRS) - foreach(PATH_DIR ${EXTRA_PATH_DIRS}) - string(APPEND BINPATH_CONTENTS - "sys.path.insert(0, '${PATH_DIR}')\n" - ) - endforeach() -endif() - -string(APPEND BINPATH_CONTENTS "from mrtrix3.app import _execute\n") - # Three possible interfaces: # 1. Standalone file residing in commands/ # 2. File stored in location commands//.py, which will contain usage() and execute() functions @@ -47,7 +38,7 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}/__init__.py") endif() elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${CMDNAME}.py") string(APPEND BINPATH_CONTENTS - "module = importlib.import_module('${CMDNAME}')\n" + "module = importlib.import_module('.${CMDNAME}', 'mrtrix3.commands')\n" "_execute(module.usage, module.execute)\n" ) else() diff --git a/docs/generate_user_docs.sh b/docs/generate_user_docs.sh index 3d07fa218b..3b76db5b36 100755 --- a/docs/generate_user_docs.sh +++ b/docs/generate_user_docs.sh @@ -42,7 +42,7 @@ function prepend { # Generating documentation for all commands mrtrix_root=$( cd "$(dirname "${BASH_SOURCE}")"/../ ; pwd -P ) -export PATH=$build_dir/bin:${mrtrix_root}/python/bin:"$PATH" +export PATH=$build_dir/bin:$PATH dirpath=${mrtrix_root}'/docs/reference/commands' export LC_ALL=C @@ -82,8 +82,8 @@ cmdlist="" for n in `find "${mrtrix_root}"/cmd/ -name "*.cpp"`; do cmdlist=$cmdlist$'\n'`basename $n` done -for n in `find "${mrtrix_root}"/python/bin/ -type f -print0 | xargs -0 grep -l "import mrtrix3"`; do - cmdlist=$cmdlist$'\n'`basename $n` +for n in `ls "${mrtrix_root}"/python/mrtrix3/commands/ --ignore=__init__.py* --ignore=CMakeLists.txt`; do + cmdlist=$cmdlist$'\n'`basename $n .py` done for n in `echo "$cmdlist" | sort`; do diff --git a/docs/reference/commands/5ttgen.rst b/docs/reference/commands/5ttgen.rst index 995f47a624..950fd9c9af 100644 --- a/docs/reference/commands/5ttgen.rst +++ b/docs/reference/commands/5ttgen.rst @@ -15,7 +15,7 @@ Usage 5ttgen algorithm [ options ] ... -- *algorithm*: Select the algorithm to be used to complete the script operation; additional details and options become available once an algorithm is nominated. Options are: freesurfer, fsl, gif, hsvs +- *algorithm*: Select the algorithm to be used; additional details and options become available once an algorithm is nominated. Options are: freesurfer, fsl, gif, hsvs Description ----------- diff --git a/docs/reference/commands/dwi2mask.rst b/docs/reference/commands/dwi2mask.rst index 26d50f6a85..6e0db85a8d 100644 --- a/docs/reference/commands/dwi2mask.rst +++ b/docs/reference/commands/dwi2mask.rst @@ -15,7 +15,7 @@ Usage dwi2mask algorithm [ options ] ... -- *algorithm*: Select the algorithm to be used to complete the script operation; additional details and options become available once an algorithm is nominated. Options are: 3dautomask, ants, b02template, consensus, fslbet, hdbet, legacy, mean, mtnorm, synthstrip, trace +- *algorithm*: Select the algorithm to be used; additional details and options become available once an algorithm is nominated. Options are: 3dautomask, ants, b02template, consensus, fslbet, hdbet, legacy, mean, mtnorm, synthstrip, trace Description ----------- diff --git a/docs/reference/commands/dwi2response.rst b/docs/reference/commands/dwi2response.rst index 23764dd20d..e835d91f76 100644 --- a/docs/reference/commands/dwi2response.rst +++ b/docs/reference/commands/dwi2response.rst @@ -15,7 +15,7 @@ Usage dwi2response algorithm [ options ] ... -- *algorithm*: Select the algorithm to be used to complete the script operation; additional details and options become available once an algorithm is nominated. Options are: dhollander, fa, manual, msmt_5tt, tax, tournier +- *algorithm*: Select the algorithm to be used; additional details and options become available once an algorithm is nominated. Options are: dhollander, fa, manual, msmt_5tt, tax, tournier Description ----------- diff --git a/docs/reference/commands/dwibiascorrect.rst b/docs/reference/commands/dwibiascorrect.rst index e8f8fdf49a..4d25516b47 100644 --- a/docs/reference/commands/dwibiascorrect.rst +++ b/docs/reference/commands/dwibiascorrect.rst @@ -15,7 +15,7 @@ Usage dwibiascorrect algorithm [ options ] ... -- *algorithm*: Select the algorithm to be used to complete the script operation; additional details and options become available once an algorithm is nominated. Options are: ants, fsl, mtnorm +- *algorithm*: Select the algorithm to be used; additional details and options become available once an algorithm is nominated. Options are: ants, fsl, mtnorm Description ----------- diff --git a/docs/reference/commands/dwinormalise.rst b/docs/reference/commands/dwinormalise.rst index f7fd5098fe..109f70722c 100644 --- a/docs/reference/commands/dwinormalise.rst +++ b/docs/reference/commands/dwinormalise.rst @@ -15,7 +15,7 @@ Usage dwinormalise algorithm [ options ] ... -- *algorithm*: Select the algorithm to be used to complete the script operation; additional details and options become available once an algorithm is nominated. Options are: group, manual, mtnorm +- *algorithm*: Select the algorithm to be used; additional details and options become available once an algorithm is nominated. Options are: group, manual, mtnorm Description ----------- diff --git a/testing/tools/CMakeLists.txt b/testing/tools/CMakeLists.txt index 8b3113e43e..946b5f7370 100644 --- a/testing/tools/CMakeLists.txt +++ b/testing/tools/CMakeLists.txt @@ -15,9 +15,12 @@ set(CPP_TOOLS_SRCS testing_npywrite.cpp ) -set(PYTHON_TOOLS_SRCS +set(PYTHON_TOOLS_STANDALONE testing_check_npy.py testing_gen_npy.py +) + +set(PYTHON_TOOLS_MRTRIXAPI testing_python_cli.py ) @@ -36,28 +39,32 @@ function(add_cpp_tool TOOL_SRC) add_dependencies(testing_tools ${TOOL_NAME}) endfunction() -function(add_python_tool TOOL_SRC IS_MRTRIX_CMD) +function(add_python_tool_standalone TOOL_SRC) get_filename_component(TOOL_NAME ${TOOL_SRC} NAME_WE) - # If the tool behaves like a Python MRtrix3 command, then we need to generate its executable using - # the MakePythonExecutable script - if(IS_MRTRIX_CMD) - set(MRTRIX_PYTHON_LIB_PATH "${PROJECT_BINARY_DIR}/lib") - add_custom_command( - TARGET testing_tools PRE_BUILD - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMAND ${CMAKE_COMMAND} -DCMDNAME=${TOOL_NAME} -DOUTPUT_DIR="${CMAKE_CURRENT_BINARY_DIR}" -DEXTRA_PATH_DIRS="${MRTRIX_PYTHON_LIB_PATH}" -P ${PROJECT_SOURCE_DIR}/cmake/MakePythonExecutable.cmake - ) - # Install "module" file alongside the executable - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${TOOL_SRC} ${CMAKE_CURRENT_BINARY_DIR}/${TOOL_SRC} COPYONLY) - else() - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${TOOL_SRC} ${CMAKE_CURRENT_BINARY_DIR}/${TOOL_NAME} COPYONLY) - endif() + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${TOOL_SRC} ${CMAKE_CURRENT_BINARY_DIR}/${TOOL_NAME} COPYONLY) +endfunction() + +# If the tool behaves like a Python MRtrix3 command, then we need to generate its executable using +# the MakePythonExecutable script +function(add_python_tool_mrtrixapi TOOL_SRC) + get_filename_component(TOOL_NAME ${TOOL_SRC} NAME_WE) + set(MRTRIX_PYTHON_LIB_PATH "${PROJECT_BINARY_DIR}/lib") + add_custom_command( + TARGET testing_tools PRE_BUILD + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND ${CMAKE_COMMAND} -DCMDNAME=${TOOL_NAME} -DOUTPUT_DIR="${PROJECT_BINARY_DIR}/bin" -P ${PROJECT_SOURCE_DIR}/cmake/MakePythonExecutable.cmake + ) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${TOOL_SRC} ${PROJECT_BINARY_DIR}/lib/mrtrix3/commands/${TOOL_SRC} COPYONLY) endfunction() foreach(CMD ${CPP_TOOLS_SRCS}) add_cpp_tool(${CMD}) endforeach(CMD) -add_python_tool(testing_check_npy.py FALSE) -add_python_tool(testing_gen_npy.py FALSE) -add_python_tool(testing_python_cli.py TRUE) +foreach(CMD ${PYTHON_TOOLS_STANDALONE}) + add_python_tool_standalone(${CMD}) +endforeach(CMD) + +foreach(CMD ${PYTHON_TOOLS_MRTRIXAPI}) + add_python_tool_mrtrixapi(${CMD}) +endforeach(CMD) diff --git a/testing/unit_tests/CMakeLists.txt b/testing/unit_tests/CMakeLists.txt index 06d56c34d4..c161343f54 100644 --- a/testing/unit_tests/CMakeLists.txt +++ b/testing/unit_tests/CMakeLists.txt @@ -73,7 +73,7 @@ function (add_bash_unit_test FILE_SRC) WORKING_DIRECTORY ${DATA_DIR} EXEC_DIRECTORIES "${EXEC_DIRS}" ENVIRONMENT "PYTHONPATH=${PYTHON_ENV_PATH}" - LABELS "unittest" + LABELS "unittest;${NAME}" ) endfunction() From 2c76370fbc5dfcf60f3ac99aedd938f9ea0a4ca3 Mon Sep 17 00:00:00 2001 From: Daljit Date: Tue, 25 Jun 2024 11:18:31 +0100 Subject: [PATCH 114/182] Disable PCHs when building macos release --- packaging/macos/build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/macos/build b/packaging/macos/build index 4f0515e9f3..03a37f9307 100755 --- a/packaging/macos/build +++ b/packaging/macos/build @@ -140,7 +140,7 @@ git clone https://github.com/MRtrix3/mrtrix3.git mrtrix3-src -b ${TAGNAME} cd ${PREFIX}-src sed -i.bak -e '90,101d' cmake/FindFFTW.cmake MRTRIX_VERSION=$(git describe --abbrev=8 | tr '-' '_') -cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/X11;/usr/X11R6" -DFFTW_USE_STATIC_LIBS=ON -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCHS} -DPNG_PNG_INCLUDE_DIR=${PREFIX}/include/QtPng -DPNG_LIBRARY=${PREFIX}/lib/libQt6BundledLibpng.a -DCMAKE_INSTALL_PREFIX=${PREFIX} +cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/X11;/usr/X11R6" -DFFTW_USE_STATIC_LIBS=ON -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCHS} -DPNG_PNG_INCLUDE_DIR=${PREFIX}/include/QtPng -DPNG_LIBRARY=${PREFIX}/lib/libQt6BundledLibpng.a -DCMAKE_INSTALL_PREFIX=${PREFIX} -DMRTRIX_USE_PCH=OFF cmake --build build cd .. MRTRIX_SECONDS=${SECONDS} From 27d377a06cdc05d5b29efa4155c0d033188c4b70 Mon Sep 17 00:00:00 2001 From: Daljit Date: Tue, 25 Jun 2024 11:21:17 +0100 Subject: [PATCH 115/182] Enable MRTRIX_BUILD_NON_CORE_STATIC for releases --- packaging/macos/build | 2 +- packaging/mingw/PKGBUILD | 2 +- packaging/package-linux-tarball.sh | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/macos/build b/packaging/macos/build index 03a37f9307..783045b4b5 100755 --- a/packaging/macos/build +++ b/packaging/macos/build @@ -140,7 +140,7 @@ git clone https://github.com/MRtrix3/mrtrix3.git mrtrix3-src -b ${TAGNAME} cd ${PREFIX}-src sed -i.bak -e '90,101d' cmake/FindFFTW.cmake MRTRIX_VERSION=$(git describe --abbrev=8 | tr '-' '_') -cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/X11;/usr/X11R6" -DFFTW_USE_STATIC_LIBS=ON -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCHS} -DPNG_PNG_INCLUDE_DIR=${PREFIX}/include/QtPng -DPNG_LIBRARY=${PREFIX}/lib/libQt6BundledLibpng.a -DCMAKE_INSTALL_PREFIX=${PREFIX} -DMRTRIX_USE_PCH=OFF +cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/X11;/usr/X11R6" -DFFTW_USE_STATIC_LIBS=ON -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCHS} -DPNG_PNG_INCLUDE_DIR=${PREFIX}/include/QtPng -DPNG_LIBRARY=${PREFIX}/lib/libQt6BundledLibpng.a -DCMAKE_INSTALL_PREFIX=${PREFIX} -DMRTRIX_USE_PCH=OFF -DMRTRIX_BUILD_NON_CORE_STATIC=ON cmake --build build cd .. MRTRIX_SECONDS=${SECONDS} diff --git a/packaging/mingw/PKGBUILD b/packaging/mingw/PKGBUILD index 0753f7cbc5..4ab03194cd 100644 --- a/packaging/mingw/PKGBUILD +++ b/packaging/mingw/PKGBUILD @@ -39,7 +39,7 @@ pkgver() { build() { cd "${_realname}" - cmake -B build -G Ninja + cmake -B build -G Ninja -DMRTRIX_BUILD_NON_CORE_STATIC=ON cmake --build build cmake --install build --prefix=./install } diff --git a/packaging/package-linux-tarball.sh b/packaging/package-linux-tarball.sh index 66600015e2..29d980185a 100755 --- a/packaging/package-linux-tarball.sh +++ b/packaging/package-linux-tarball.sh @@ -66,7 +66,8 @@ cmake -B $build_dir -S $source_dir \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=$install_dir \ -DCMAKE_EXE_LINKER_FLAGS="-Wl,--as-needed" \ - -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DMRTRIX_BUILD_NON_CORE_STATIC=ON cmake --build $build_dir cmake --install $build_dir From 38cc99267332515f855be2e01d5242cb9eaad859 Mon Sep 17 00:00:00 2001 From: Ben Jeurissen Date: Wed, 26 Jun 2024 09:58:20 +0200 Subject: [PATCH 116/182] Modified FindFFTW to only look for double precision version of fftw3, preventing failures when unused single and quad precision are not available --- cmake/FindFFTW.cmake | 37 ++----------------------------------- packaging/macos/build | 1 - 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/cmake/FindFFTW.cmake b/cmake/FindFFTW.cmake index 42fdc41ff7..3524a1bc22 100644 --- a/cmake/FindFFTW.cmake +++ b/cmake/FindFFTW.cmake @@ -54,22 +54,6 @@ if( FFTW_ROOT ) NO_DEFAULT_PATH ) - find_library( - FFTWF_LIB - NAMES "fftw3f" - PATHS ${FFTW_ROOT} - PATH_SUFFIXES "lib" "lib64" - NO_DEFAULT_PATH - ) - - find_library( - FFTWL_LIB - NAMES "fftw3l" - PATHS ${FFTW_ROOT} - PATH_SUFFIXES "lib" "lib64" - NO_DEFAULT_PATH - ) - #find includes find_path( FFTW_INCLUDES @@ -87,19 +71,6 @@ else() PATHS ${PKG_FFTW_LIBRARY_DIRS} ${LIB_INSTALL_DIR} ) - find_library( - FFTWF_LIB - NAMES "fftw3f" - PATHS ${PKG_FFTW_LIBRARY_DIRS} ${LIB_INSTALL_DIR} - ) - - - find_library( - FFTWL_LIB - NAMES "fftw3l" - PATHS ${PKG_FFTW_LIBRARY_DIRS} ${LIB_INSTALL_DIR} - ) - find_path( FFTW_INCLUDES NAMES "fftw3.h" @@ -108,11 +79,7 @@ else() endif() -set(FFTW_LIBRARIES ${FFTW_LIB} ${FFTWF_LIB}) - -if(FFTWL_LIB) - set(FFTW_LIBRARIES ${FFTW_LIBRARIES} ${FFTWL_LIB}) -endif() +set(FFTW_LIBRARIES ${FFTW_LIB}) set( CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES_SAV} ) @@ -120,5 +87,5 @@ include(FindPackageHandleStandardArgs) find_package_handle_standard_args(FFTW DEFAULT_MSG FFTW_INCLUDES FFTW_LIBRARIES) -mark_as_advanced(FFTW_INCLUDES FFTW_LIBRARIES FFTW_LIB FFTWF_LIB FFTWL_LIB) +mark_as_advanced(FFTW_INCLUDES FFTW_LIBRARIES FFTW_LIB) diff --git a/packaging/macos/build b/packaging/macos/build index 783045b4b5..a8825e4063 100755 --- a/packaging/macos/build +++ b/packaging/macos/build @@ -138,7 +138,6 @@ export PATH SECONDS=0 git clone https://github.com/MRtrix3/mrtrix3.git mrtrix3-src -b ${TAGNAME} cd ${PREFIX}-src -sed -i.bak -e '90,101d' cmake/FindFFTW.cmake MRTRIX_VERSION=$(git describe --abbrev=8 | tr '-' '_') cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=${PREFIX} -DCMAKE_IGNORE_PREFIX_PATH="/opt/homebrew;/usr/X11;/usr/X11R6" -DFFTW_USE_STATIC_LIBS=ON -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCHS} -DPNG_PNG_INCLUDE_DIR=${PREFIX}/include/QtPng -DPNG_LIBRARY=${PREFIX}/lib/libQt6BundledLibpng.a -DCMAKE_INSTALL_PREFIX=${PREFIX} -DMRTRIX_USE_PCH=OFF -DMRTRIX_BUILD_NON_CORE_STATIC=ON cmake --build build From 07c529025c15b0546a6e82f1bd7d9601875b65db Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 17 Jul 2024 10:05:43 +1000 Subject: [PATCH 117/182] New command: dirrotate --- cmd/dirrotate.cpp | 162 ++++++++++++++++++++++++++ docs/reference/commands/dirrotate.rst | 78 +++++++++++++ docs/reference/commands_list.rst | 2 + 3 files changed, 242 insertions(+) create mode 100644 cmd/dirrotate.cpp create mode 100644 docs/reference/commands/dirrotate.rst diff --git a/cmd/dirrotate.cpp b/cmd/dirrotate.cpp new file mode 100644 index 0000000000..4b4aac7823 --- /dev/null +++ b/cmd/dirrotate.cpp @@ -0,0 +1,162 @@ +/* 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 "command.h" +#include "dwi/directions/file.h" +#include "file/utils.h" +#include "math/rng.h" +#include "progressbar.h" +#include "thread.h" + +constexpr size_t default_permutations = 1e8; + +using namespace MR; +using namespace App; + +// clang-format off +void usage() { + + AUTHOR = "Robert E. Smith (robert.smith@florey.edu.au)"; + + SYNOPSIS = "Apply a rotation to a direction set"; + + DESCRIPTION + + "The primary use case of this command is to find," + " for a given basis direction set," + " an appropriate rotation that preserves the homogeneity of coverage on the sphere" + " but that minimises the maximal peak amplitude along the physical axes of the scanner," + " so as to minimise the peak gradient system demands." + " It can alternatively be used to introduce a random rotation" + " to hopefully prevent any collinearity between directions in different shells," + " by requesting only a single permutation."; + + ARGUMENTS + + Argument ("in", "the input direction file").type_file_in() + + Argument ("out", "the output direction file").type_file_out(); + + + OPTIONS + + Option ("permutations", "number of permutations to try" + " (default: " + str(default_permutations) + ")") + + Argument ("num").type_integer(1) + + + Option ("cartesian", "Output the directions in Cartesian coordinates [x y z]" + " instead of [az el]."); +} +// clang-format on + +using axis_type = Eigen::Matrix; +using rotation_type = Eigen::AngleAxis; +using rotation_transform_type = Eigen::Transform; + +class Shared { +public: + Shared(const Eigen::MatrixXd &directions, size_t target_num_permutations) + : directions(directions), + target_num_permutations(target_num_permutations), + num_permutations(0), + progress(target_num_permutations == 1 ? "randomising direction set orientation" : "optimising directions for peak gradient load", target_num_permutations), + best_rotation(0.0, axis_type{0.0, 0.0, 0.0}), + min_peak(1.0), + original_peak(directions.array().abs().maxCoeff()) {} + + bool update(default_type peak, const rotation_type &rotation) { + std::lock_guard lock(mutex); + if (peak < min_peak) { + min_peak = peak; + best_rotation = rotation; + progress.set_text( + std::string(target_num_permutations == 1 ? "randomising direction set orientation" : "optimising directions for peak gradient load") + + " (original = " + str(original_peak, 6) + "; " + + (target_num_permutations == 1 ? "rotated" : "best") + + " = " + str(min_peak, 6) + ")"); + } + ++num_permutations; + ++progress; + return num_permutations < target_num_permutations; + } + + default_type peak(const rotation_type &rotation) const { + return (rotation_transform_type(rotation).linear() * directions.transpose()).array().abs().maxCoeff(); + } + + const rotation_type &get_best_rotation() const { return best_rotation; } + +protected: + const Eigen::MatrixXd &directions; + const size_t target_num_permutations; + size_t num_permutations; + ProgressBar progress; + rotation_type best_rotation; + default_type min_peak; + default_type original_peak; + std::mutex mutex; +}; + +class Processor { +public: + Processor(Shared &shared) : + shared(shared), + rotation(0.0, axis_type{0.0, 0.0, 0.0}), + angle_distribution (-Math::pi, Math::pi), + axes_distribution (-1.0, 1.0) {} + + void execute() { + while (eval()); + } + + bool eval() { + rotation.angle() = angle_distribution(rng); + do { + axis[0] = axes_distribution(rng); + axis[1] = axes_distribution(rng); + axis[2] = axes_distribution(rng); + } while (axis.squaredNorm() > 1.0); + rotation.axis() = axis.normalized(); + + return shared.update(shared.peak(rotation), rotation); + } + +protected: + Shared &shared; + rotation_type rotation; + axis_type axis; + Math::RNG rng; + std::uniform_real_distribution angle_distribution; + std::uniform_real_distribution axes_distribution; +}; + +void run() { + auto directions = DWI::Directions::load_cartesian(argument[0]); + + size_t num_permutations = get_option_value("permutations", default_permutations); + + rotation_type rotation; + { + Shared shared(directions, num_permutations); + if (num_permutations == 1) { + Processor processor(shared); + processor.eval(); + } else { + Thread::run(Thread::multi(Processor(shared)), "eval thread"); + } + rotation = shared.get_best_rotation(); + } + + directions = (rotation_transform_type(rotation).linear() * directions.transpose()).transpose(); + + DWI::Directions::save(directions, argument[1], !get_options("cartesian").empty()); +} diff --git a/docs/reference/commands/dirrotate.rst b/docs/reference/commands/dirrotate.rst new file mode 100644 index 0000000000..154f4aebfd --- /dev/null +++ b/docs/reference/commands/dirrotate.rst @@ -0,0 +1,78 @@ +.. _dirrotate: + +dirrotate +=================== + +Synopsis +-------- + +Apply a rotation to a direction set + +Usage +-------- + +:: + + dirrotate [ options ] in out + +- *in*: the input direction file +- *out*: the output direction file + +Description +----------- + +The primary use case of this command is to find, for a given basis direction set, an appropriate rotation that preserves the homogeneity of coverage on the sphere but that minimises the maximal peak amplitude along the physical axes of the scanner, so as to minimise the peak gradient system demands. It can alternatively be used to introduce a random rotation to hopefully prevent any collinearity between directions in different shells, by requesting only a single permutation. + +Options +------- + +- **-permutations num** number of permutations to try (default: 100000000) + +- **-cartesian** Output the directions in Cartesian coordinates [x y z] instead of [az el]. + +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/docs/reference/commands_list.rst b/docs/reference/commands_list.rst index c29f39b65d..78cfaa506e 100644 --- a/docs/reference/commands_list.rst +++ b/docs/reference/commands_list.rst @@ -28,6 +28,7 @@ List of MRtrix3 commands commands/dirgen commands/dirmerge commands/dirorder + commands/dirrotate commands/dirsplit commands/dirstat commands/dwi2adc @@ -160,6 +161,7 @@ List of MRtrix3 commands |cpp.png|, :ref:`dirgen`, "Generate a set of uniformly distributed directions using a bipolar electrostatic repulsion model" |cpp.png|, :ref:`dirmerge`, "Splice / merge multiple sets of directions in such a way as to maintain near-optimality upon truncation" |cpp.png|, :ref:`dirorder`, "Reorder a set of directions to ensure near-uniformity upon truncation" + |cpp.png|, :ref:`dirrotate`, "Apply a rotation to a direction set" |cpp.png|, :ref:`dirsplit`, "Split a set of evenly distributed directions (as generated by dirgen) into approximately uniformly distributed subsets" |cpp.png|, :ref:`dirstat`, "Report statistics on a direction set" |cpp.png|, :ref:`dwi2adc`, "Calculate ADC and/or IVIM parameters." From 49f7ede0b0698d8f1c2a4c3ee1a48753b22df38b Mon Sep 17 00:00:00 2001 From: Robert Smith Date: Wed, 17 Jul 2024 10:18:11 +1000 Subject: [PATCH 118/182] dirmerge: Better selection of first volume Previously, the first volume in the output scheme was chosen as a random volume from the first subset of the first shell. Here, this is modified to instead be a random volume from the largest subset of the largest shell. This should slightly improve the quality of distribution of bmax volumes across the acquisition time. --- cmd/dirmerge.cpp | 49 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/cmd/dirmerge.cpp b/cmd/dirmerge.cpp index 49396aec93..4ec4d4904f 100644 --- a/cmd/dirmerge.cpp +++ b/cmd/dirmerge.cpp @@ -111,10 +111,45 @@ void run() { }(); INFO("found total of " + str(total) + " volumes"); - // pick random direction from first direction set: + // pick which volume will be first + // from within the shell with the largest number of volumes, + // choose the subset with the largest number of volumes, + // then choose a random volume within that subset + size_t largest_shell = [&] { + value_type largestshell_b = 0.0; + size_t largestshell_n = 0; + size_t largestshell_index = 0; + for (size_t n = 0; n != dirs.size(); ++n) { + size_t size = 0; + for (auto &m : dirs[n]) + size += m.size(); + if (size > largestshell_n || (size == largestshell_n && bvalue[n] > largestshell_b)) { + largestshell_b = bvalue[n]; + largestshell_n = size; + largestshell_index = n; + } + } + return largestshell_index; + }(); + INFO("first volume will be from shell b=" + str(bvalue[largest_shell])); + size_t largest_subset_within_largest_shell = [&] { + size_t largestsubset_index = 0; + size_t largestsubset_n = dirs[largest_shell][0].size(); + for (size_t n = 1; n != dirs[largest_shell].size(); ++n) { + const size_t size = dirs[largest_shell][n].size(); + if (size > largestsubset_n) { + largestsubset_n = size; + largestsubset_index = n; + } + } + return largestsubset_index; + }(); + if (num_subsets > 1) { + INFO("first volume will be from subset " + str(largest_subset_within_largest_shell + 1) + " from largest shell"); + } std::random_device rd; std::mt19937 rng(rd()); - size_t first = std::uniform_int_distribution(0, dirs[0][0].size() - 1)(rng); + const size_t first = std::uniform_int_distribution(0, dirs[largest_shell][largest_subset_within_largest_shell].size() - 1)(rng); std::vector merged; @@ -161,11 +196,6 @@ void run() { fraction.push_back(float(n) / float(total)); }; - push(0, 0, first); - - std::vector counts(bvalue.size(), 0); - ++counts[0]; - auto num_for_b = [&](size_t b) { size_t n = 0; for (auto &d : merged) @@ -174,6 +204,11 @@ void run() { return n; }; + // write the volume that was chosen to be first + push(largest_shell, largest_subset_within_largest_shell, first); + std::vector counts(bvalue.size(), 0); + ++counts[largest_shell]; + size_t nPE = num_subsets > 1 ? 1 : 0; while (merged.size() < total) { // find shell with shortfall in numbers: From e1f0c99cafd56982216171cf8382e0ff25522c25 Mon Sep 17 00:00:00 2001 From: Daljit Singh Date: Tue, 30 Jul 2024 09:27:51 +0100 Subject: [PATCH 119/182] Add interface library for project-wide compile options Previously we were relying on the use of add_compile_options and add_compile_definitions for defining compiler definitions and options for the entire project. This commit replaces that behaviour by defining a new mrtrix::common INTERFACE library that links to all targets in the project (the library is linked to mrtrix::core). This is more idiomatic and prevents the propagation of compiler definitions to targets that do not link to mrtrix::core (useful when compiling MRtrix3 as a subproject of anothe project). --- CMakeLists.txt | 40 ++++++++++++++++++++-------------------- cmd/CMakeLists.txt | 2 +- core/CMakeLists.txt | 1 + 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1299b0794..00f44a6246 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,35 +81,35 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git AND NOT EXISTS ${CMAKE_CURRENT_SOURCE "and then run `pre-commit install` from the ${CMAKE_CURRENT_SOURCE_DIR} directory.") endif() - -# Allow compilation of big object of files in debug mode on MINGW -if(MINGW AND CMAKE_BUILD_TYPE MATCHES "Debug") - add_compile_options(-Wa,-mbig-obj) -endif() - -add_compile_definitions( +add_library(mrtrix-common INTERFACE) +add_library(mrtrix::common ALIAS mrtrix-common) +target_compile_definitions(mrtrix-common INTERFACE MRTRIX_BUILD_TYPE="${CMAKE_BUILD_TYPE}" $<$:MRTRIX_WINDOWS> $<$:MRTRIX_MACOSX> $<$:MRTRIX_FREEBSD> ) -if(MRTRIX_STL_DEBUGGING AND $>) - if(MSVC) - add_compile_definitions(_ITERATOR_DEBUG_LEVEL=1) - elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(_LIBCPP_DEBUG) - elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") - add_compile_options(_GLIBCXX_DEBUG _GLIBCXX_DEBUG_PEDANTIC) - endif() +if(MRTRIX_STL_DEBUGGING AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + message(STATUS "Enabling STL debug mode") + target_compile_definitions(mrtrix-common INTERFACE + $<$:_ITERATOR_DEBUG_LEVEL=1> + $<$:_GLIBCXX_DEBUG _GLIBCXX_DEBUG_PEDANTIC> + $<$:_LIBCPP_DEBUG=1> + ) endif() if(MRTRIX_WARNINGS_AS_ERRORS) - if (MSVC) - add_compile_options(/WX) - else() - add_compile_options(-Werror) - endif() + message(STATUS "Enabling warnings as errors") + target_compile_options(mrtrix-common INTERFACE + $<$:/WX> + $<$:-Werror> + ) +endif() + +# Allow compilation of big object of files in debug mode on MINGW +if(MINGW AND CMAKE_BUILD_TYPE MATCHES "Debug") + target_compile_options(mrtrix-common INTERFACE -Wa,-mbig-obj) endif() diff --git a/cmd/CMakeLists.txt b/cmd/CMakeLists.txt index fd6de1b333..9c28ba9897 100644 --- a/cmd/CMakeLists.txt +++ b/cmd/CMakeLists.txt @@ -10,7 +10,7 @@ if(MRTRIX_USE_PCH) add_executable(pch_cmd ${CMAKE_CURRENT_BINARY_DIR}/pch_cmd.cpp) target_include_directories(pch_cmd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../core) find_package(Eigen3 REQUIRED) - target_link_libraries(pch_cmd PRIVATE Eigen3::Eigen) + target_link_libraries(pch_cmd PRIVATE Eigen3::Eigen mrtrix::common) target_precompile_headers(pch_cmd PRIVATE [["app.h"]] [["image.h"]] diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 7cc54e9388..54cec5b4eb 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -87,6 +87,7 @@ target_link_libraries(mrtrix-core PUBLIC ZLIB::ZLIB ${FFTW_LIBRARIES} mrtrix::core-version-lib + mrtrix::common Threads::Threads ${FFTW_LIBRARIES} ) From 79d4edf97edb0170bab605727ffa4da10335306e Mon Sep 17 00:00:00 2001 From: Daljit Date: Fri, 3 May 2024 14:52:18 +0100 Subject: [PATCH 120/182] Use string_view in dash detection/removal --- core/mrtrix.cpp | 15 +++++++++------ core/mrtrix.h | 9 +++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/mrtrix.cpp b/core/mrtrix.cpp index 2e99b40365..3a316b6095 100644 --- a/core/mrtrix.cpp +++ b/core/mrtrix.cpp @@ -233,15 +233,18 @@ size_t char_is_dash(const char *arg) { return 0; } -bool is_dash(const std::string &arg) { - const size_t nbytes = char_is_dash(arg.c_str()); +bool is_dash(std::string_view arg) { + const size_t nbytes = char_is_dash(arg.data()); return nbytes != 0 && nbytes == arg.size(); } -bool consume_dash(const char *&arg) { - size_t nbytes = char_is_dash(arg); - arg += nbytes; - return nbytes != 0; +std::string_view without_leading_dash(std::string_view arg) { + size_t nbytes = char_is_dash(arg.data()); + while (nbytes > 0) { + arg.remove_prefix(nbytes); + nbytes = char_is_dash(arg.data()); + } + return arg; } std::string join(const std::vector &V, const std::string &delimiter) { diff --git a/core/mrtrix.h b/core/mrtrix.h index 34bc754011..4a637b9a56 100644 --- a/core/mrtrix.h +++ b/core/mrtrix.h @@ -104,13 +104,10 @@ bool match(const std::string &pattern, const std::string &text, bool ignore_case size_t char_is_dash(const char *arg); //! match whole string to a dash or any Unicode character that looks like one -bool is_dash(const std::string &arg); +bool is_dash(std::string_view arg); -//! match current character to a dash or any Unicode character that looks like one -/*! \note If a match is found, this also advances the \a arg pointer to the next - * character in the string, which could be one or several bytes along depending on - * the width of the UTF8 character identified. */ -bool consume_dash(const char *&arg); +//! returns string without leading dashes +std::string_view without_leading_dash(std::string_view arg); template inline std::string str(const T &value, int precision = 0) { std::ostringstream stream; From 3ab8fd968099b5071b50117375db8326ceda88ff Mon Sep 17 00:00:00 2001 From: Daljit Date: Tue, 21 May 2024 11:54:59 +0100 Subject: [PATCH 121/182] Replace union with std::variant for argument choices --- core/cmdline_option.h | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/core/cmdline_option.h b/core/cmdline_option.h index bae7b52b3d..f8610739ac 100644 --- a/core/cmdline_option.h +++ b/core/cmdline_option.h @@ -26,6 +26,7 @@ #include "mrtrix.h" #include "types.h" +#include namespace MR::App { @@ -101,13 +102,11 @@ class Argument { /*! this is used to construct a command-line argument object, with a name * and description. If default arguments are used, the object corresponds * to the end-of-list specifier, as detailed in \ref command_line_parsing. */ - Argument(const char *name = nullptr, std::string description = std::string()) - : id(name), desc(description), type(Undefined), flags(None) { - memset(&limits, 0x00, sizeof(limits)); - } + Argument(std::string name, std::string description = std::string()) + : id(std::move(name)), desc(std::move(description)), type(Undefined), flags(None) {} //! the argument name - const char *id; + std::string id; //! the argument description std::string desc; //! the argument type @@ -115,16 +114,16 @@ class Argument { //! the argument flags (AllowMultiple & Optional) ArgFlags flags; + struct IntRange { + int64_t min, max; + }; + struct FloatRange { + default_type min, max; + }; + //! a structure to store the various parameters of the Argument - union { - const char *const *choices; - struct { - int64_t min, max; - } i; - struct { - default_type min, max; - } f; - } limits; + using Limits = std::variant, IntRange, FloatRange>; + Limits limits; operator bool() const { return id; } @@ -178,8 +177,7 @@ class Argument { const int64_t max = std::numeric_limits::max()) { assert(type == Undefined); type = Integer; - limits.i.min = min; - limits.i.max = max; + limits = IntRange{min, max}; return *this; } @@ -197,8 +195,7 @@ class Argument { const default_type max = std::numeric_limits::infinity()) { assert(type == Undefined); type = Float; - limits.f.min = min; - limits.f.max = max; + limits = FloatRange{min, max}; return *this; } @@ -216,7 +213,7 @@ class Argument { Argument &type_choice(const char *const *choices) { assert(type == Undefined); type = Choice; - limits.choices = choices; + limits = choices; return *this; } From b0f80182abb7ea42a37c634ffae0e7bcbbd7861e Mon Sep 17 00:00:00 2001 From: Daljit Date: Fri, 7 Jun 2024 10:55:32 +0100 Subject: [PATCH 122/182] Use std::string for app CLI --- core/app.cpp | 218 ++++++++++++++++++++++-------------------- core/app.h | 27 +++--- core/cmdline_option.h | 25 ++--- 3 files changed, 142 insertions(+), 128 deletions(-) diff --git a/core/app.cpp b/core/app.cpp index 3dec82b14b..73fb6523bd 100644 --- a/core/app.cpp +++ b/core/app.cpp @@ -74,8 +74,8 @@ OptionGroup __standard_options = + Option("version", "display version information and exit."); // clang-format on -const char *AUTHOR = nullptr; -const char *COPYRIGHT = "Copyright (c) 2008-2024 the MRtrix3 contributors.\n" +std::string AUTHOR{}; +std::string COPYRIGHT = "Copyright (c) 2008-2024 the MRtrix3 contributors.\n" "\n" "This Source Code Form is subject to the terms of the Mozilla Public\n" "License, v. 2.0. If a copy of the MPL was not distributed with this\n" @@ -89,7 +89,7 @@ const char *COPYRIGHT = "Copyright (c) 2008-2024 the MRtrix3 contributors.\n" "See the Mozilla Public License v. 2.0 for more details.\n" "\n" "For more details, see http://www.mrtrix.org/.\n"; -const char *SYNOPSIS = nullptr; +std::string SYNOPSIS{}; std::string NAME; std::string command_history_string; @@ -116,8 +116,7 @@ const char *project_version = nullptr; const char *project_build_date = nullptr; const char *executable_uses_mrtrix_version = nullptr; -int argc = 0; -const char *const *argv = nullptr; +std::vector raw_arguments_list; bool overwrite_files = false; void (*check_overwrite_files_func)(const std::string &name) = nullptr; @@ -450,12 +449,16 @@ std::string Argument::usage() const { case Undefined: assert(0); break; - case Integer: - stream << "INT " << limits.i.min << " " << limits.i.max; + case Integer: { + const auto int_range = std::get(limits); + stream << "INT " << int_range.min << " " << int_range.max; break; - case Float: - stream << "FLOAT " << limits.f.min << " " << limits.f.max; + } + case Float: { + const auto float_range = std::get(this->limits); + stream << "FLOAT " << float_range.min << " " << float_range.max; break; + } case Text: stream << "TEXT"; break; @@ -471,11 +474,13 @@ std::string Argument::usage() const { case ArgDirectoryOut: stream << "DIROUT"; break; - case Choice: + case Choice: { stream << "CHOICE"; - for (const char *const *p = limits.choices; *p; ++p) - stream << " " << *p; + const auto &choices = std::get>(limits); + for (const std::string &p : choices) + stream << " " << p; break; + } case ImageIn: stream << "IMAGEIN"; break; @@ -807,7 +812,7 @@ std::string restructured_text_usage() { while (OPTIONS[n].name != group_names[i]) ++n; if (OPTIONS[n].name != std::string("OPTIONS")) - s += OPTIONS[n].name + std::string("\n") + std::string(std::strlen(OPTIONS[n].name), '^') + "\n\n"; + s += OPTIONS[n].name + std::string("\n") + std::string(OPTIONS[n].name.size(), '^') + "\n\n"; while (n < OPTIONS.size()) { if (OPTIONS[n].name == group_names[i]) { for (size_t o = 0; o < OPTIONS[n].size(); ++o) @@ -831,66 +836,73 @@ std::string restructured_text_usage() { } s += std::string(MRTRIX_CORE_REFERENCE) + "\n\n"; - s += std::string("--------------\n\n") + "\n\n**Author:** " + (char *)AUTHOR + "\n\n**Copyright:** " + COPYRIGHT + - "\n\n"; + s += std::string("--------------\n\n") + "\n\n**Author:** " + AUTHOR + "\n\n**Copyright:** " + COPYRIGHT + "\n\n"; return s; } -const Option *match_option(const char *arg) { - if (consume_dash(arg) && *arg && !isdigit(*arg) && *arg != '.') { - while (consume_dash(arg)) - ; - std::vector candidates; - std::string root(arg); +const Option *match_option(std::string_view arg) { + // let's check if arg starts with a dash + auto no_dash_arg = without_leading_dash(arg); + if (arg.size() == no_dash_arg.size() || no_dash_arg.empty() || isdigit(no_dash_arg.front()) != 0 || + no_dash_arg.front() == '.') { + return nullptr; + } - for (size_t i = 0; i < OPTIONS.size(); ++i) - get_matches(candidates, OPTIONS[i], root); - get_matches(candidates, __standard_options, root); + std::vector candidates; + std::string root(no_dash_arg); - // no matches - if (candidates.empty()) - throw Exception(std::string("unknown option \"-") + root + "\""); + for (size_t i = 0; i < OPTIONS.size(); ++i) + get_matches(candidates, OPTIONS[i], root); + get_matches(candidates, __standard_options, root); - // return match if unique: - if (candidates.size() == 1) - return candidates[0]; + // no matches + if (candidates.empty()) + throw Exception(std::string("unknown option \"-") + root + "\""); - // return match if fully specified: - for (size_t i = 0; i < candidates.size(); ++i) - if (root == candidates[i]->id) - return candidates[i]; + // return match if unique: + if (candidates.size() == 1) + return candidates[0]; - // check if there is only one *unique* candidate - const auto cid = candidates[0]->id; - if (std::all_of(++candidates.begin(), candidates.end(), [&cid](const Option *cand) { return cand->id == cid; })) - return candidates[0]; + // return match if fully specified: + for (size_t i = 0; i < candidates.size(); ++i) + if (root == candidates[i]->id) + return candidates[i]; - // report something useful: - root = "several matches possible for option \"-" + root + "\": \"-" + candidates[0]->id; + // check if there is only one *unique* candidate + const auto cid = candidates[0]->id; + if (std::all_of(++candidates.begin(), candidates.end(), [&cid](const Option *cand) { return cand->id == cid; })) + return candidates[0]; - for (size_t i = 1; i < candidates.size(); ++i) - root += std::string("\", \"-") + candidates[i]->id + "\""; + // report something useful: + root = "several matches possible for option \"-" + root + "\": \"-" + candidates[0]->id; - throw Exception(root); - } + for (size_t i = 1; i < candidates.size(); ++i) + root += std::string("\", \"-") + candidates[i]->id + "\""; - return nullptr; + throw Exception(root); } -void sort_arguments(int argc, const char *const *argv) { - for (int n = 1; n < argc; ++n) { - if (argv[n]) { - const Option *opt = match_option(argv[n]); - if (opt) { - if (n + int(opt->size()) >= argc) - throw Exception(std::string("not enough parameters to option \"-") + opt->id + "\""); - - option.push_back(ParsedOption(opt, argv + n + 1)); - n += opt->size(); - } else - argument.push_back(ParsedArgument(nullptr, nullptr, argv[n])); +void sort_arguments(const std::vector &arguments) { + auto it = arguments.begin(); + while (it != arguments.end()) { + const Option *opt = match_option(*it); + if (opt != nullptr) { + if (it + opt->size() >= arguments.end()) { + throw Exception(std::string("not enough parameters to option \"-") + opt->id + "\""); + } + + const size_t index = std::distance(arguments.begin(), it); + std::vector option_args; + std::copy_n(it + 1, opt->size(), std::back_inserter(option_args)); + std::transform(option_args.begin(), option_args.end(), option_args.begin(), without_leading_dash); + + option.push_back(ParsedOption(opt, option_args)); + it += opt->size(); + } else { + argument.push_back(ParsedArgument(nullptr, nullptr, *it)); } + ++it; } } @@ -908,28 +920,30 @@ void parse_standard_options() { } void verify_usage() { - if (!AUTHOR) + if (AUTHOR.empty()) throw Exception("No author specified for command " + std::string(NAME)); - if (!SYNOPSIS) + if (SYNOPSIS.empty()) throw Exception("No synopsis specified for command " + std::string(NAME)); } void parse_special_options() { - if (argc != 2) + // special options are only accessible as the first argument + if (raw_arguments_list.size() != 1) return; - if (strcmp(argv[1], "__print_full_usage__") == 0) { + + if (raw_arguments_list.front() == "__print_full_usage__") { print(full_usage()); throw 0; } - if (strcmp(argv[1], "__print_usage_markdown__") == 0) { + if (raw_arguments_list.front() == "__print_usage_markdown__") { print(markdown_usage()); throw 0; } - if (strcmp(argv[1], "__print_usage_rst__") == 0) { + if (raw_arguments_list.front() == "__print_usage_rst__") { print(restructured_text_usage()); throw 0; } - if (strcmp(argv[1], "__print_synopsis__") == 0) { + if (raw_arguments_list.front() == "__print_synopsis__") { print(SYNOPSIS); throw 0; } @@ -939,7 +953,7 @@ void parse() { argument.clear(); option.clear(); - sort_arguments(argc, argv); + sort_arguments(raw_arguments_list); if (!get_options("help").empty()) { print_help(); @@ -1128,10 +1142,10 @@ void init(int cmdline_argc, const char *const *cmdline_argv) { terminal_use_colour = !ProgressBar::set_update_method(); - argc = cmdline_argc; - argv = cmdline_argv; + raw_arguments_list = std::vector(cmdline_argv, cmdline_argv + cmdline_argc); + NAME = Path::basename(raw_arguments_list.front()); + raw_arguments_list.erase(raw_arguments_list.begin()); - NAME = Path::basename(argv[0]); #ifdef MRTRIX_WINDOWS if (Path::has_suffix(NAME, ".exe")) NAME.erase(NAME.size() - 4); @@ -1170,9 +1184,9 @@ void init(int cmdline_argc, const char *const *cmdline_argv) { } return s; }; - command_history_string = argv[0]; - for (int n = 1; n < argc; ++n) - command_history_string += std::string(" ") + argv_quoted(argv[n]); + command_history_string = raw_arguments_list.front(); + for (const auto &a : raw_arguments_list) + command_history_string += std::string(" ") + argv_quoted(a); command_history_string += std::string(" (version=") + mrtrix_version; if (project_version) command_history_string += std::string(", project=") + project_version; @@ -1206,15 +1220,15 @@ int64_t App::ParsedArgument::as_int() const { bool alpha_is_last = false; bool contains_dotpoint = false; char alpha_char = 0; - for (const char *c = p; *c; ++c) { - if (std::isalpha(*c)) { + for (const char &c : p) { + if (std::isalpha(c) != 0) { ++alpha_count; alpha_is_last = true; - alpha_char = *c; + alpha_char = c; } else { alpha_is_last = false; } - if (*c == '.') + if (c == '.') contains_dotpoint = true; } if (alpha_count > 1) @@ -1262,16 +1276,15 @@ int64_t App::ParsedArgument::as_int() const { retval = to(p); } - const int64_t min = arg->limits.i.min; - const int64_t max = arg->limits.i.max; - if (retval < min || retval > max) { + const auto range = std::get(arg->limits); + if (retval < range.min || retval > range.max) { std::string msg("value supplied for "); if (opt) msg += std::string("option \"") + opt->id; else msg += std::string("argument \"") + arg->id; - msg += "\" is out of bounds (valid range: " + str(min) + " to " + str(max) + ", value supplied: " + str(retval) + - ")"; + msg += "\" is out of bounds (valid range: " + str(range.min) + " to " + str(range.max) + + ", value supplied: " + str(retval) + ")"; throw Exception(msg); } return retval; @@ -1279,19 +1292,18 @@ int64_t App::ParsedArgument::as_int() const { if (arg->type == Choice) { std::string selection = lowercase(p); - const char *const *choices = arg->limits.choices; - for (int i = 0; choices[i]; ++i) { - if (selection == choices[i]) { - return i; - } + const auto &choices = std::get>(arg->limits); + auto it = std::find(choices.begin(), choices.end(), selection); + if (it == choices.end()) { + std::string msg = std::string("unexpected value supplied for "); + if (opt != nullptr) + msg += std::string("option \"") + opt->id; + else + msg += std::string("argument \"") + arg->id; + msg += std::string("\" (received \"" + std::string(p) + "\"; valid choices are: ") + join(choices, ", ") + ")"; + throw Exception(msg); } - std::string msg = std::string("unexpected value supplied for "); - if (opt) - msg += std::string("option \"") + opt->id; - else - msg += std::string("argument \"") + arg->id; - msg += std::string("\" (received \"" + std::string(p) + "\"; valid choices are: ") + join(choices, ", ") + ")"; - throw Exception(msg); + return static_cast(std::distance(choices.begin(), it)); } assert(0); return (0); @@ -1300,16 +1312,15 @@ int64_t App::ParsedArgument::as_int() const { default_type App::ParsedArgument::as_float() const { assert(arg->type == Float); const default_type retval = to(p); - const default_type min = arg->limits.f.min; - const default_type max = arg->limits.f.max; - if (retval < min || retval > max) { + const auto range = std::get(arg->limits); + if (retval < range.min || retval > range.max) { std::string msg("value supplied for "); if (opt) msg += std::string("option \"") + opt->id; else msg += std::string("argument \"") + arg->id; - msg += - "\" is out of bounds (valid range: " + str(min) + " to " + str(max) + ", value supplied: " + str(retval) + ")"; + msg += "\" is out of bounds (valid range: " + str(range.min) + " to " + str(range.max) + + ", value supplied: " + str(retval) + ")"; throw Exception(msg); } @@ -1346,9 +1357,9 @@ std::vector ParsedArgument::as_sequence_float() const { return std::vector(); } -ParsedArgument::ParsedArgument(const Option *option, const Argument *argument, const char *text) - : opt(option), arg(argument), p(text) { - assert(text); +ParsedArgument::ParsedArgument(const Option *option, const Argument *argument, std::string text, size_t index) + : opt(option), arg(argument), p(std::move(text)), index_(index){ + assert(!p.empty()); } void ParsedArgument::error(Exception &e) const { @@ -1370,10 +1381,11 @@ void check_overwrite(const std::string &name) { } } -ParsedOption::ParsedOption(const Option *option, const char *const *arguments) : opt(option), args(arguments) { + +ParsedOption::ParsedOption(const Option *option, const std::vector &arguments) : opt(option), args(arguments) { for (size_t i = 0; i != option->size(); ++i) { - const char *p = arguments[i]; - if (!consume_dash(p)) + const auto &p = arguments[i]; + if (!is_dash(p)) continue; if (((*option)[i].type == ImageIn || (*option)[i].type == ImageOut) && is_dash(arguments[i])) continue; diff --git a/core/app.h b/core/app.h index f8a8e30310..70741bcd6d 100644 --- a/core/app.h +++ b/core/app.h @@ -46,8 +46,7 @@ extern bool fail_on_warn; extern bool terminal_use_colour; extern const std::thread::id main_thread_ID; -extern int argc; -extern const char *const *argv; +extern std::vector raw_arguments_list; extern const char *project_version; extern const char *project_build_date; @@ -130,10 +129,10 @@ void parse_special_options(); void parse(); //! sort command-line tokens into arguments and options [used internally] -void sort_arguments(int argc, const char *const *argv); +void sort_arguments(const std::vector &arguments); //! uniquely match option stub to Option -const Option *match_option(const char *stub); +const Option *match_option(std::string_view arg); //! dump formatted help page [used internally] std::string full_usage(); @@ -142,7 +141,7 @@ class ParsedArgument { public: operator std::string() const { return p; } - const char *as_text() const { return p; } + const std::string &as_text() const { return p; } bool as_bool() const { return to(p); } int64_t as_int() const; uint64_t as_uint() const { return uint64_t(as_int()); } @@ -167,14 +166,14 @@ class ParsedArgument { operator std::vector() const { return as_sequence_uint(); } operator std::vector() const { return as_sequence_float(); } - const char *c_str() const { return p; } + const char *c_str() const { return p.c_str(); } private: const Option *opt; const Argument *arg; - const char *p; + std::string p; - ParsedArgument(const Option *option, const Argument *argument, const char *text); + ParsedArgument(const Option *option, const Argument *argument, std::string text, size_t index); void error(Exception &e) const; @@ -182,7 +181,7 @@ class ParsedArgument { friend class Options; friend void MR::App::init(int argc, const char *const *argv); friend void MR::App::parse(); - friend void MR::App::sort_arguments(int argc, const char *const *argv); + friend void MR::App::sort_arguments(const std::vector &arguments); }; //! object storing information about option parsed from command-line @@ -190,12 +189,12 @@ class ParsedArgument { * returned by App::get_options(). */ class ParsedOption { public: - ParsedOption(const Option *option, const char *const *arguments); + ParsedOption(const Option *option, const std::vector &arguments); //! reference to the corresponding Option entry in the OPTIONS section const Option *opt; //! pointer into \c argv corresponding to the option's first argument - const char *const *args; + std::vector args; ParsedArgument operator[](size_t num) const; @@ -285,13 +284,13 @@ extern OptionList OPTIONS; extern bool REQUIRES_AT_LEAST_ONE_ARGUMENT; //! set the author of the command -extern const char *AUTHOR; +extern std::string AUTHOR; //! set the copyright notice if different from that used in MRtrix -extern const char *COPYRIGHT; +extern std::string COPYRIGHT; //! set a one-sentence synopsis for the command -extern const char *SYNOPSIS; +extern std::string SYNOPSIS; //! add references to command help page /*! Like the description, use the '+' operator to add paragraphs (typically diff --git a/core/cmdline_option.h b/core/cmdline_option.h index f8610739ac..22cdc61d18 100644 --- a/core/cmdline_option.h +++ b/core/cmdline_option.h @@ -19,6 +19,8 @@ #include #include #include +#include +#include #ifdef None #undef None @@ -125,7 +127,7 @@ class Argument { using Limits = std::variant, IntRange, FloatRange>; Limits limits; - operator bool() const { return id; } + operator bool() const { return id.empty(); } //! specifies that the argument is optional /*! For example: @@ -200,17 +202,17 @@ class Argument { } //! specifies that the argument should be selected from a predefined list - /*! The list of allowed values must be specified as a nullptr-terminated - * list of C strings. Here is an example usage: + /*! The list of allowed values must be specified as a vector of strings. + * Here is an example usage: * \code - * const char* mode_list [] = { "standard", "pedantic", "approx", nullptr }; + * const std::vector mode_list = { "standard", "pedantic", "approx" }; * * ARGUMENTS * + Argument ("mode", "the mode of operation") * .type_choice (mode_list); * \endcode * \note Each string in the list must be supplied in \b lowercase. */ - Argument &type_choice(const char *const *choices) { + Argument &type_choice(const std::vector &choices) { assert(type == Undefined); type = Choice; limits = choices; @@ -323,18 +325,19 @@ class Argument { */ class Option : public std::vector { public: - Option() : id(nullptr), flags(Optional) {} + Option() : flags(Optional) {} - Option(const char *name, const std::string &description) : id(name), desc(description), flags(Optional) {} + Option(std::string name, const std::string &description) + : id(std::move(name)), desc(std::move(description)), flags(Optional) {} Option &operator+(const Argument &arg) { push_back(arg); return *this; } - operator bool() const { return id; } + operator bool() const { return id.empty(); } //! the option name - const char *id; + std::string id; //! the option description std::string desc; //! option flags (AllowMultiple and/or Optional) @@ -388,8 +391,8 @@ class Option : public std::vector { */ class OptionGroup : public std::vector