Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python CLI changes for Windows image piping #2969

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/formats/pipe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ std::unique_ptr<ImageIO::Base> Pipe::read(Header &H) const {
SignalHandler::mark_file_for_deletion(H.name());

if (!Path::has_suffix(H.name(), ".mif"))
throw Exception("MRtrix only supports the .mif format for command-line piping");
throw Exception("MRtrix only supports the .mif format for command-line piping"
" (received \"" + H.name() + "\")");

std::unique_ptr<ImageIO::Base> original_handler(mrtrix_handler.read(H));
std::unique_ptr<ImageIO::Pipe> io_handler(new ImageIO::Pipe(std::move(*original_handler)));
Expand Down
207 changes: 132 additions & 75 deletions python/mrtrix3/app.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion python/mrtrix3/commands/dwi2mask/b02template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.Parser.make_userpath_object(app.Parser._UserPathExtras, app.ARGS.ants_options) # pylint: disable=protected-access
ants_options_as_path = app.FSQEPath(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:
Expand Down
5 changes: 2 additions & 3 deletions python/mrtrix3/commands/dwi2response/dhollander.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# For more details, see http://www.mrtrix.org/.


import math, shlex, shutil
import math, shutil
from mrtrix3 import CONFIG, MRtrixError
from mrtrix3 import app, image, run

Expand Down Expand Up @@ -303,8 +303,7 @@ def execute(): #pylint: disable=unused-variable
app.warn('Single-fibre WM response function selection algorithm "tax" will not honour requested WM voxel percentage')
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,
+ f' -scratch {app.SCRATCH_DIR}{recursive_cleanup_option}',
show=False)
else:
app.console(' Selecting WM single-fibre voxels using built-in (Dhollander et al., 2019) algorithm')
Expand Down
6 changes: 2 additions & 4 deletions python/mrtrix3/commands/dwi2response/msmt_5tt.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,11 @@ def execute(): #pylint: disable=unused-variable
if not app.ARGS.sfwm_fa_threshold:
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)
f' -scratch {app.SCRATCH_DIR}{recursive_cleanup_option}')
else:
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)
f' -scratch {app.SCRATCH_DIR}{recursive_cleanup_option}')

# Check for empty masks
wm_voxels = image.statistics('wm_sf_mask.mif', mask='wm_sf_mask.mif').count
Expand Down
32 changes: 19 additions & 13 deletions python/mrtrix3/commands/dwinormalise/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import os
from mrtrix3 import MRtrixError
from mrtrix3 import app, image, path, run, utils
from mrtrix3 import app, image, path, run


FA_THRESHOLD_DEFAULT = 0.4
Expand Down Expand Up @@ -97,7 +97,7 @@ def __init__(self, filename, prefix, mask_filename = ''):

app.activate_scratch_dir()

utils.make_dir('fa')
os.makedirs('fa')
progress = app.ProgressBar('Computing FA images', len(input_list))
for i in input_list:
run.command(['dwi2tensor', os.path.join(app.ARGS.input_dir, i.filename), '-mask', os.path.join(app.ARGS.mask_dir, i.mask_filename), '-', '|',
Expand All @@ -122,22 +122,28 @@ def __init__(self, filename, prefix, mask_filename = ''):
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(app.ARGS.output_dir)
utils.make_dir('wm_mask_warped')
app.ARGS.output_dir.mkdir()
os.makedirs('wm_mask_warped')
os.makedirs('dwi_normalised')
for i in input_list:
run.command(['mrtransform', 'template_wm_mask.mif', os.path.join('wm_mask_warped', f'{i.prefix}.mif'),
in_path = os.path.join(app.ARGS.input_dir, i.filename)
warp_path = os.path.join('warps', f'{i.prefix}.mif')
fa_path = os.path.join('fa', f'{i.prefix}.mif')
wm_mask_warped_path = os.path.join('wm_mask_warped', f'{i.prefix}.mif')
dwi_normalised_path = os.path.join('dwi_normalised', f'{i.prefix}.mif')
run.command(['mrtransform', 'template_wm_mask.mif', wm_mask_warped_path,
'-interp', 'nearest',
'-warp_full', os.path.join('warps', f'{i.prefix}.mif'),
'-warp_full', warp_path,
'-from', '2',
'-template', os.path.join('fa', f'{i.prefix}.mif')])
'-template', fa_path])
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),
in_path,
wm_mask_warped_path,
dwi_normalised_path])
run.command(['mrconvert', dwi_normalised_path, app.ARGS.output_dir / i.filename],
mrconvert_keyval=in_path,
force=app.FORCE_OVERWRITE)
os.remove('temp.mif')
app.cleanup([warp_path, fa_path, wm_mask_warped_path, dwi_normalised_path])
progress.increment()
progress.done()

Expand Down
6 changes: 3 additions & 3 deletions python/mrtrix3/commands/population_template/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,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.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
app.ARGS.input_dir.append(app.Parser.UserInPath(inargs[0]))
app.ARGS.template.append(app.Parser.UserOutPath(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:
Expand Down Expand Up @@ -366,7 +366,7 @@ def execute(): #pylint: disable=unused-variable

if use_masks:
os.mkdir('mask_transformed')
write_log = (app.VERBOSITY >= 2)
write_log = app.VERBOSITY >= 2
if write_log:
os.mkdir('log')

Expand Down
3 changes: 1 addition & 2 deletions python/mrtrix3/commands/population_template/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@

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(',')]
return [app.Parser.UserDirOutPath(item) for item in input_value.split(',')]
@staticmethod
def _legacytypestring():
return 'SEQDIROUT'
Expand Down
13 changes: 5 additions & 8 deletions python/mrtrix3/commands/population_template/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#
# For more details, see http://www.mrtrix.org/.

import csv, math, os, re, shutil, sys
import csv, math, os, re, shutil
from mrtrix3 import MRtrixError
from mrtrix3 import app, image, path, run, utils
from . import IMAGEEXT
Expand All @@ -33,10 +33,7 @@ def copy(src, dst, follow_symlinks=True): # pylint: disable=unused-variable
"""
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)
shutil.copyfile(src, dst, follow_symlinks=follow_symlinks) # pylint: disable=unexpected-keyword-arg
return dst


Expand All @@ -52,16 +49,16 @@ def check_linear_transformation(transformation, cmd, max_scaling=0.5, max_shear=
return True
data = utils.load_keyval(f'{transformation}decomp')
run.function(os.remove, f'{transformation}decomp')
scaling = [float(value) for value in data['scaling']]
scaling = [float(value) for value in data['scaling'].split()]
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']]
shear = [float(value) for value in data['shear'].split()]
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])
rot_angle = float(data['angle_axis'].split()[0])
if abs(rot_angle) > max_rot:
app.warn(f'large rotation ({rot_angle}) in {transformation}')
good = False
Expand Down
90 changes: 48 additions & 42 deletions python/mrtrix3/fsl.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@
#
# For more details, see http://www.mrtrix.org/.

import os, shutil
import os, pathlib, shutil
from mrtrix3 import MRtrixError




_SUFFIX = ''

IMAGETYPE2SUFFIX = {'NIFTI': '.nii',
'NIFTI_GZ': '.nii.gz',
'NIFTI_PAIR': '.img',
'NIFTI_PAIR_GZ': None}



# Functions that may be useful for scripts that interface with FMRIB FSL tools
Expand All @@ -30,8 +35,8 @@
# the output files to appear.
def check_first(prefix, structures): #pylint: disable=unused-variable
from mrtrix3 import app, path #pylint: disable=import-outside-toplevel
vtk_files = [ prefix + '-' + struct + '_first.vtk' for struct in structures ]
existing_file_count = sum(os.path.exists(filename) for filename in vtk_files)
vtk_files = [ pathlib.Path(f'{prefix}-{struct}_first.vtk') for struct in structures ]
existing_file_count = sum(vtk_file.exists() for vtk_file 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; '
Expand All @@ -43,7 +48,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 {os.path.join(app.SCRATCH_DIR, "first.logs")})')
f'(check {app.SCRATCH_DIR / "first.logs"})')



Expand All @@ -64,15 +69,17 @@ def eddy_binary(cuda): #pylint: disable=unused-variable
# select the one with the highest version number
binaries = [ ]
for directory in os.environ['PATH'].split(os.pathsep):
if os.path.isdir(directory):
for entry in os.listdir(directory):
if entry.startswith('eddy_cuda'):
binaries.append(entry)
directory = pathlib.Path(directory)
try:
for entry in directory.glob('eddy_cuda*'):
binaries.append(entry)
except OSError:
pass
max_version = 0.0
exe_path = ''
exe_path = None
for entry in binaries:
try:
version = float(entry.lstrip('eddy_cuda'))
version = float(entry.stem.lstrip('eddy_cuda'))
if version > max_version:
max_version = version
exe_path = entry
Expand Down Expand Up @@ -113,21 +120,26 @@ def exe_name(name): #pylint: disable=unused-variable


# In some versions of FSL, even though we try to predict the names of image files that
# FSL commands will generate based on the suffix() function, the FSL binaries themselves
# ignore the FSLOUTPUTTYPE environment variable. Therefore, the safest approach is:
# Whenever receiving an output image from an FSL command, explicitly search for the path
# FSL commands will generate based on the suffix() function,
# the FSL binaries themselves ignore the FSLOUTPUTTYPE environment variable.
# Therefore, the safest approach is:
# Whenever receiving an output image from an FSL command,
# explicitly search for the path
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(f'Image at expected location: "{prefix}{suffix()}"')
return f'{prefix}{suffix()}'
prefix = pathlib.PurePath(name)
prefix = prefix.parent / prefix.name.split('.')[0]
expected = prefix.with_suffix(suffix())
if expected.is_file():
app.debug(f'Image at expected location: {expected}')
return expected
for suf in ['.nii', '.nii.gz', '.img']:
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}"')
candidate = prefix.with_suffix(suf)
if candidate.is_file():
app.debug(f'Expected image at {expected}, '
f'but found at {candidate}')
return candidate
raise MRtrixError(f'Unable to find FSL output image for path {name}')



Expand All @@ -141,26 +153,20 @@ def suffix(): #pylint: disable=unused-variable
if _SUFFIX:
return _SUFFIX
fsl_output_type = os.environ.get('FSLOUTPUTTYPE', '')
if fsl_output_type == 'NIFTI':
app.debug('NIFTI -> .nii')
_SUFFIX = '.nii'
elif fsl_output_type == 'NIFTI_GZ':
app.debug('NIFTI_GZ -> .nii.gz')
_SUFFIX = '.nii.gz'
elif fsl_output_type == 'NIFTI_PAIR':
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')
elif fsl_output_type:
app.warn('Unrecognised value for environment variable FSLOUTPUTTYPE '
f'("{fsl_output_type}"): '
'Expecting compressed NIfTIs, but FSL commands may fail')
_SUFFIX = '.nii.gz'
if fsl_output_type in IMAGETYPE2SUFFIX:
_SUFFIX = IMAGETYPE2SUFFIX[fsl_output_type]
if _SUFFIX is None:
raise MRtrixError(f'MRtrix3 does not support FSL output image type "{fsl_output_type}; '
'please change FSLOUTPUTTYPE environment variable')
app.debug(f'{fsl_output_type} -> {_SUFFIX}')
else:
app.warn('Environment variable FSLOUTPUTTYPE not set; '
'FSL commands may fail, '
'or script may fail to locate FSL command outputs')
_SUFFIX = '.nii.gz'
if fsl_output_type:
app.warn('Unrecognised value for environment variable FSLOUTPUTTYPE '
f'("{fsl_output_type}"): '
'executed FSL commands may fail')
else:
app.warn('Environment variable FSLOUTPUTTYPE not set; '
'FSL commands may fail, '
'or this script may fail to locate FSL command outputs')
return _SUFFIX
Loading
Loading