Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • paulmc/fslpy
  • ndcn0236/fslpy
  • seanf/fslpy
3 results
Show changes
Showing
with 1202 additions and 115 deletions
#!/usr/bin/env python
#
# Text2Vest.py - Convert an ASCII text matrix file into a VEST file.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""``Text2Vest`` simply takes a plain text ASCII text matrix file, and
adds a VEST header.
"""
import sys
import numpy as np
import fsl.data.vest as fslvest
usage = "Usage: Text2Vest <text_file> <vest_file>"
def main(argv=None):
"""Convert a plain text file to a VEST file. """
if argv is None:
argv = sys.argv[1:]
if len(argv) != 2:
print(usage)
return 0
infile, outfile = argv
data = np.loadtxt(infile, ndmin=2)
vest = fslvest.generateVest(data)
with open(outfile, 'wt') as f:
f.write(vest)
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# Vest2Text.py - Convert a VEST matrix file into a plain text ASCII file.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""``Vest2Text`` takes a VEST file containing a 2D matrix, and converts it
into a plain-text ASCII file.
"""
import sys
import numpy as np
import fsl.data.vest as fslvest
usage = "Usage: Vest2Text <vest_file> <text_file>"
def main(argv=None):
"""Convert a VEST file to a plain text file. """
if argv is None:
argv = sys.argv[1:]
if len(argv) != 2:
print(usage)
return 0
infile, outfile = argv
data = fslvest.loadVestFile(infile)
if np.issubdtype(data.dtype, np.integer): fmt = '%d'
else: fmt = '%0.12f'
np.savetxt(outfile, data, fmt=fmt)
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# __init__.py - The fsl.scripts package.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The ``fsl.scripts`` package contains all of the executable scripts provided
by ``fslpy``.
"""
......@@ -19,20 +19,9 @@ import warnings
import logging
import numpy as np
# if h5py <= 2.7.1 is installed,
# it will be imported via nibabel,
# and will cause a numpy warning
# to be emitted.
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
import fsl.data.image as fslimage
# If wx is not present, then fsl.utils.platform
# will complain that it is not present.
logging.getLogger('fsl.utils.platform').setLevel(logging.ERROR)
import fsl.data.atlases as fslatlases # noqa
import fsl.version as fslversion # noqa
import fsl.data.image as fslimage
import fsl.data.atlases as fslatlases
import fsl.version as fslversion
log = logging.getLogger(__name__)
......@@ -381,7 +370,7 @@ def maskQuery(atlas, masks, *args, **kwargs):
labels = []
props = []
zprops = atlas.maskProportions(mask)
zprops = atlas.maskValues(mask)
for i in range(len(zprops)):
if zprops[i] > 0:
......@@ -405,7 +394,7 @@ def coordQuery(atlas, coords, voxel, *args, **kwargs):
if isinstance(atlas, fslatlases.ProbabilisticAtlas):
props = atlas.proportions(coord, voxel=voxel)
props = atlas.values(coord, voxel=voxel)
labels = []
nzprops = []
......@@ -569,10 +558,10 @@ def parseArgs(args):
usages = {
'main' : 'usage: atlasq [-h] command [options]',
'ohi' : textwrap.dedent("""
usage: atlasq ohi -h
atlasq ohi --dumpatlases
atlasq ohi -a atlas -c X,Y,Z
atlasq ohi -a atlas -m mask
usage: atlasquery -h
atlasquery --dumpatlases
atlasquery -a atlas -c X,Y,Z
atlasquery -a atlas -m mask
""").strip(),
'list' : 'usage: atlasq list [-e]',
'summary' : 'usage: atlasq summary atlas',
......
#!/usr/bin/env python
#
# extract_noise.py - Deprecated - replaced by fsl_ents.py
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module is deprecated - it has been replaced by :mod:`.fsl_ents`. """
import sys
if __name__ == '__main__':
print('extract_noise is deprecated and will be removed in fslpy 2.0. '
'Use fsl_ents instead.')
from fsl.scripts.fsl_ents import main
sys.exit(main())
#!/usr/bin/env python
#
# fsl_abspath.py - Make a path absolute
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The fsl_abspath command - makes relative paths absolute.
"""
import os.path as op
import sys
usage = """
usage: fsl_abspath path
""".strip()
def main(argv=None):
"""fsl_abspath - make a relative path absolute. """
if argv is None:
argv = sys.argv[1:]
if len(argv) != 1:
print(usage)
return 1
print(op.realpath(argv[0]))
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# fsl_apply_x5.py - Apply an X5 transformation to an image.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The ``fsl_apply_x5`` script can be used to apply an X5 transformation file
to resample an image.
"""
import functools as ft
import sys
import argparse
import fsl.transform.x5 as x5
import fsl.transform.nonlinear as nonlinear
import fsl.utils.parse_data as parse_data
import fsl.utils.image.resample as resample
import fsl.data.image as fslimage
def parseArgs(args):
"""Parses command-line arguments.
:arg args: Sequence of command-line arguments. If ``None``, ``sys.argv``
is used
:returns: An ``argparse.Namespace`` object containing parsed arguments
"""
parser = argparse.ArgumentParser('fsl_apply_x5')
flags = {
'input' : ('input',),
'xform' : ('xform',),
'output' : ('output',),
'interp' : ('-i', '--interp'),
'ref' : ('-r', '--ref'),
}
helps = {
'input' : 'Input image',
'xform' : 'X5 transformation file',
'output' : 'Output image',
'interp' : 'Interpolation (default: linear)',
'ref' : 'Alternate reference image (default: '
'reference specified in X5 file)',
}
opts = {
'input' : dict(help=helps['input'],
type=parse_data.Image),
'xform' : dict(help=helps['xform']),
'output' : dict(help=helps['output'],
type=parse_data.ImageOut),
'interp' : dict(help=helps['interp'],
choices=('nearest', 'linear', 'cubic'),
default='linear'),
'ref' : dict(help=helps['ref'],
type=parse_data.Image),
}
parser.add_argument(*flags['input'], **opts['input'])
parser.add_argument(*flags['xform'], **opts['xform'])
parser.add_argument(*flags['output'], **opts['output'])
parser.add_argument(*flags['interp'], **opts['interp'])
parser.add_argument(*flags['ref'], **opts['ref'])
if len(args) == 0:
parser.print_help()
sys.exit(0)
args = parser.parse_args(args)
if args.interp == 'nearest': args.interp = 0
elif args.interp == 'linear': args.interp = 1
elif args.interp == 'cubic': args.interp = 3
return args
def applyLinear(args):
"""Applies a linear X5 transformation file to the input.
:arg args: ``argparse.Namespace`` object
:returns: The transformed input as an :class:`.Image` object
"""
input = args.input
xform, src, ref = x5.readLinearX5(args.xform)
if args.ref is not None:
ref = args.ref
res, xform = resample.resampleToReference(input,
ref,
matrix=xform,
order=args.interp)
return fslimage.Image(res, xform=xform, header=ref.header)
def applyNonlinear(args):
"""Applies a non-linear X5 transformation file to the input.
:arg args: ``argparse.Namespace`` object
:returns: The transformed input as an :class:`.Image` object
"""
field = x5.readNonLinearX5(args.xform)
if args.ref is None: ref = field.ref
else: ref = args.ref
result = nonlinear.applyDeformation(args.input,
field,
ref=ref,
order=args.interp,
mode='constant')
return fslimage.Image(result, header=ref.header)
def main(args=None):
"""Entry point. Parse command-line arguments, then calls
:func:`applyLinear` or :func:`applyNonlinear` depending on the x5 file
type.
"""
if args is None:
args = sys.argv[1:]
print()
print('Warning: this version of fsl_apply_x5 is a development release.\n'
'Interface, behaviour, and input/output formats of future versions\n'
'may differ substantially from this version.')
print()
args = parseArgs(args)
if x5.inferType(args.xform) == 'linear':
result = applyLinear(args)
else:
result = applyNonlinear(args)
result.save(args.output)
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# fsl_convert_x5.py - Convert between FSL and X5 transformation files.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This script can be used to convert between FSL and X5 linear and non-linear
transformation file formats.
"""
import os.path as op
import functools as ft
import textwrap as tw
import sys
import shutil
import logging
import argparse
import fsl.data.image as fslimage
import fsl.utils.parse_data as parse_data
import fsl.transform.flirt as flirt
import fsl.transform.fnirt as fnirt
import fsl.transform.x5 as x5
log = logging.getLogger(__name__)
def parseArgs(args):
"""Create an argument parser and parse the given ``args``.
:arg args: Sequence of commane line arguments.
:return: An ``argparse.Namespace`` object containing parsed arguments.
"""
helps = {
'input' : 'Input file',
'output' : 'Output file',
'source' : 'Source image',
'reference' : 'Reference image',
'input_format' : 'Input format - if not provided, the input format '
'is automatically inferred from the input file '
'name.',
'output_format' : 'Output format - if not provided, the output format '
'is automatically inferred from the output file '
'name.',
}
epilog = tw.dedent("""
If the input and output file formats are the same, the input file
is simply copied to the output file.
""").strip()
parser = argparse.ArgumentParser('fsl_convert_x5')
subparsers = parser.add_subparsers(dest='ctype')
flirt = subparsers.add_parser('flirt', epilog=epilog)
fnirt = subparsers.add_parser('fnirt', epilog=epilog)
imgtype = parse_data.Image
flirt.add_argument('input', help=helps['input'])
flirt.add_argument('output', help=helps['output'])
flirt.add_argument('-s', '--source', help=helps['source'],
type=imgtype)
flirt.add_argument('-r', '--reference', help=helps['reference'],
type=imgtype)
flirt.add_argument('-if', '--input_format', help=helps['input_format'],
choices=('x5', 'mat'))
flirt.add_argument('-of', '--output_format', help=helps['output_format'],
choices=('x5', 'mat'))
fnirt .add_argument('input', help=helps['input'])
fnirt .add_argument('output', help=helps['output'])
fnirt .add_argument('-s', '--source', help=helps['source'],
type=imgtype)
fnirt .add_argument('-r', '--reference', help=helps['reference'],
type=imgtype)
fnirt .add_argument('-if', '--input_format', help=helps['input_format'],
choices=('x5', 'nii'))
fnirt .add_argument('-of', '--output_format', help=helps['output_format'],
choices=('x5', 'nii'))
if len(args) == 0:
parser.print_help()
sys.exit(0)
if len(args) == 1:
if args[0] == 'flirt':
flirt.print_help()
sys.exit(0)
elif args[0] == 'fnirt':
fnirt.print_help()
sys.exit(0)
args = parser.parse_args(args)
# If input/output formats were not
# specified, infer them from the
# file names
def getfmt(arg, fname, exist):
ext = op.splitext(fname)[1]
if ext in ('.mat', '.x5'):
return ext[1:]
fname = fslimage.addExt(fname, mustExist=exist)
if fslimage.looksLikeImage(fname):
return 'nii'
parser.error('Could not infer format from '
'filename: {}'.format(args.input))
if args.input_format is None:
args.input_format = getfmt('input', args.input, True)
if args.output_format is None:
args.output_format = getfmt('output', args.output, False)
# The source and reference arguments are
# required if the input is a FLIRT matrix
# or a FNIRT displacement/coefficient field.
if args.input_format in ('mat', 'nii') and \
(args.source is None or args.reference is None):
parser.error('You must specify a source and reference '
'when the input is not an X5 file!')
return args
def flirtToX5(args):
"""Convert a linear FLIRT transformation matrix to an X5 transformation
file.
"""
src = args.source
ref = args.reference
xform = flirt.readFlirt(args.input)
xform = flirt.fromFlirt(xform, src, ref, 'world', 'world')
x5.writeLinearX5(args.output, xform, src, ref)
def X5ToFlirt(args):
"""Convert a linear X5 transformation file to a FLIRT matrix. """
xform, src, ref = x5.readLinearX5(args.input)
xform = flirt.toFlirt(xform, src, ref, 'world', 'world')
flirt.writeFlirt(xform, args.output)
def fnirtToX5(args):
"""Convert a non-linear FNIRT transformation into an X5 transformation
file.
"""
src = args.source
ref = args.reference
field = fnirt.readFnirt(args.input, src=src, ref=ref)
field = fnirt.fromFnirt(field, 'world', 'world')
x5.writeNonLinearX5(args.output, field)
def X5ToFnirt(args):
"""Convert a non-linear X5 transformation file to a FNIRT-style
transformation file.
"""
field = x5.readNonLinearX5(args.input)
field = fnirt.toFnirt(field)
field.save(args.output)
def doFlirt(args):
"""Converts a linear FIRT transformation file to or from the X5 format. """
infmt = args.input_format
outfmt = args.output_format
if (infmt, outfmt) == ('x5', 'mat'): X5ToFlirt(args)
elif (infmt, outfmt) == ('mat', 'x5'): flirtToX5(args)
else: shutil.copy(args.input, args.output)
def doFnirt(args):
"""Converts a non-linear FNIRT transformation file to or from the X5
format.
"""
infmt = args.input_format
outfmt = args.output_format
if (infmt, outfmt) == ('x5', 'nii'): X5ToFnirt(args)
elif (infmt, outfmt) == ('nii', 'x5'): fnirtToX5(args)
else: shutil.copy(args.input, args.output)
def main(args=None):
"""Entry point. Calls :func:`doFlirt` or :func:`doFnirt` depending on
the sub-command specified in the arguments.
:arg args: Sequence of command-line arguments. If not provided,
``sys.argv`` is used.
"""
if args is None:
args = sys.argv[1:]
print()
print('Warning: this version of fsl_convert_x5 is a development release.\n'
'Interface, behaviour, and input/output formats of future versions\n'
'may differ substantially from this version.')
print()
args = parseArgs(args)
if args.ctype == 'flirt': doFlirt(args)
elif args.ctype == 'fnirt': doFnirt(args)
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# extract_noise.py - Extract ICA component time courses from a MELODIC
# directory.
# fsl_ents.py - Extract ICA component time courses from a MELODIC directory.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
......@@ -10,8 +9,6 @@ time series from a MELODIC ``.ica`` directory.
"""
from __future__ import print_function
import os.path as op
import sys
import argparse
......@@ -19,12 +16,8 @@ import warnings
import numpy as np
# See atlasq.py for explanation
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
import fsl.data.fixlabels as fixlabels
import fsl.data.melodicanalysis as melanalysis
import fsl.data.fixlabels as fixlabels
import fsl.data.melodicanalysis as melanalysis
DTYPE = np.float64
......
......@@ -12,19 +12,13 @@ The :func:`main` function is essentially a wrapper around the
"""
from __future__ import print_function
import os.path as op
import sys
import warnings
import logging
import fsl.utils.path as fslpath
# See atlasq.py for explanation
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
import fsl.utils.imcp as imcp
import fsl.data.image as fslimage
import fsl.utils.imcp as imcp
import fsl.data.image as fslimage
usage = """Usage:
......@@ -59,7 +53,13 @@ def main(argv=None):
print(usage)
return 1
# When converting to NIFTI2, nibabel
# emits an annoying message via log.warning:
# sizeof_hdr should be 540; set sizeof_hdr to 540
logging.getLogger('nibabel').setLevel(logging.ERROR)
try:
srcs = [fslimage.fixExt(s) for s in srcs]
srcs = fslpath.removeDuplicates(
srcs,
allowedExts=fslimage.ALLOWED_EXTENSIONS,
......
......@@ -9,17 +9,11 @@ NIFTI/ANALYZE image files.
"""
from __future__ import print_function
import itertools as it
import glob
import sys
import warnings
import fsl.utils.path as fslpath
# See atlasq.py for explanation
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
import fsl.data.image as fslimage
usage = """
Usage: imglob [-extension/extensions] <list of names>
......@@ -27,8 +21,17 @@ Usage: imglob [-extension/extensions] <list of names>
-extensions for image list with full extensions
""".strip()
exts = fslimage.ALLOWED_EXTENSIONS
groups = fslimage.FILE_GROUPS
# The lists below are defined in the
# fsl.data.image class, but are duplicated
# here for performance (to avoid import of
# nibabel/numpy/etc).
exts = ['.nii.gz', '.nii', '.img', '.hdr', '.img.gz', '.hdr.gz']
"""List of supported image file extensions. """
groups = [('.hdr', '.img'), ('.hdr.gz', '.img.gz')]
"""List of known image file groups (image/header file pairs). """
def imglob(paths, output=None):
......@@ -61,12 +64,38 @@ def imglob(paths, output=None):
imgfiles = []
# Expand any wildcard paths if provided.
# Depending on the way that imglob is
# invoked, this may not get done by the
# calling shell.
#
# We also have to handle incomplete
# wildcards, e.g. if the user provides
# "img_??", we need to add possible
# file suffixes before it can be
# expanded.
expanded = []
for path in paths:
if any(c in path for c in '*?[]'):
if fslpath.hasExt(path, exts):
globs = [path]
else:
globs = [f'{path}{ext}' for ext in exts]
globs = [glob.glob(g) for g in globs]
expanded.extend(it.chain(*globs))
else:
expanded.append(path)
paths = expanded
# Build a list of all image files (both
# hdr and img and otherwise) that match
for path in paths:
try:
path = fslimage.removeExt(path)
imgfiles.extend(fslimage.addExt(path, unambiguous=False))
path = fslpath.removeExt(path, allowedExts=exts)
imgfiles.extend(fslpath.addExt(path,
allowedExts=exts,
unambiguous=False))
except fslpath.PathError:
continue
......
#!/usr/bin/env python
#
# imln.py - Create symbolic links to image files.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module defines the ``imln`` application, for creating sym-links
to NIFTI image files.
.. note:: When creating links to relative paths, ln requires that the path is
relative to the link location, rather than the invocation
location. This is *not* currently supported by imln, and possibly
never will be.
"""
import os.path as op
import os
import sys
import fsl.utils.path as fslpath
# The lists below are defined in the
# fsl.data.image class, but are duplicated
# here for performance (to avoid import of
# nibabel/numpy/etc).
exts = ['.nii.gz', '.nii',
'.img', '.hdr',
'.img.gz', '.hdr.gz',
'.mnc', '.mnc.gz']
"""List of file extensions that are supported by ``imtest``.
"""
groups = [('.hdr', '.img'), ('.hdr.gz', '.img.gz')]
"""List of known image file groups (image/header file pairs). """
usage = """
Usage: imln <file1> <file2>
Makes a link (called file2) to file1
NB: filenames can be basenames or include an extension
""".strip()
def main(argv=None):
"""``imln`` - create sym-links to images. """
if argv is None:
argv = sys.argv[1:]
if len(argv) != 2:
print(usage)
return 1
target, linkbase = argv
target = fslpath.removeExt(target, exts)
linkbase = fslpath.removeExt(linkbase, exts)
# Target must exist, so we can
# infer the correct extension(s).
# Error on incomplete file groups
# (e.g. a.img without a.hdr).
try:
targets = fslpath.getFileGroup(target,
allowedExts=exts,
fileGroups=groups,
unambiguous=True)
except Exception as e:
print(f'Error: {e}')
return 1
for target in targets:
if not op.exists(target):
continue
ext = fslpath.getExt(target, exts)
link = f'{linkbase}{ext}'
try:
# emulate old imln behaviour - if
# link already exists, it is removed
if op.exists(link):
os.remove(link)
os.symlink(target, link)
except Exception as e:
print(f'Error: {e}')
return 1
return 0
if __name__ == '__main__':
sys.exit(main())
......@@ -13,19 +13,13 @@ The :func:`main` function is essentially a wrapper around the
"""
from __future__ import print_function
import os.path as op
import sys
import warnings
import logging
import fsl.utils.path as fslpath
# See atlasq.py for explanation
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
import fsl.utils.imcp as imcp
import fsl.data.image as fslimage
import fsl.utils.imcp as imcp
import fsl.data.image as fslimage
usage = """Usage:
......@@ -60,7 +54,13 @@ def main(argv=None):
print(usage)
return 1
# When converting to NIFTI2, nibabel
# emits an annoying message via log.warning:
# sizeof_hdr should be 540; set sizeof_hdr to 540
logging.getLogger('nibabel').setLevel(logging.ERROR)
try:
srcs = [fslimage.fixExt(s) for s in srcs]
srcs = fslpath.removeDuplicates(
srcs,
allowedExts=fslimage.ALLOWED_EXTENSIONS,
......
#!/usr/bin/env python
#
# imrm.py - Remove image files.
#
# Author: Paul McCarthy <paulmc@fmrib.ox.ac.uk>
#
"""This module defines the ``imrm`` application, for removing NIFTI image
files.
"""
import os.path as op
import os
import sys
import fsl.scripts.imglob as imglob
usage = """Usage: imrm <list of image names to remove>
NB: filenames can be basenames or not
""".strip()
def main(argv=None):
"""Removes all images which are specified on the command line. """
if argv is None:
argv = sys.argv[1:]
if len(argv) < 1:
print(usage)
return 1
paths = imglob.imglob(argv, 'all')
for path in paths:
if op.exists(path):
os.remove(path)
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# imtest.py - Test whether an image file exists or not.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The ``imtest`` script can be used to test whether an image file exists or
not, without having to know the file suffix (.nii, .nii.gz, etc).
"""
import os.path as op
import sys
import fsl.utils.path as fslpath
# The lists below are defined in the
# fsl.data.image class, but are duplicated
# here for performance (to avoid import of
# nibabel/numpy/etc).
exts = ['.nii.gz', '.nii',
'.img', '.hdr',
'.img.gz', '.hdr.gz',
'.mnc', '.mnc.gz']
"""List of file extensions that are supported by ``imtest``.
"""
groups = [('.hdr', '.img'), ('.hdr.gz', '.img.gz')]
"""List of known image file groups (image/header file pairs). """
def imtest(path):
"""Returns ``True`` if the given image path exists, False otherwise. """
path = fslpath.removeExt(path, exts)
path = op.realpath(path)
# getFileGroup will raise an error
# if the image (including all
# components - i.e. header and
# image) does not exist
try:
fslpath.getFileGroup(path,
allowedExts=exts,
fileGroups=groups,
unambiguous=True)
return True
except fslpath.PathError:
return False
def main(argv=None):
"""Test if an image path exists, and prints ``'1'`` if it does or ``'0'``
if it doesn't.
"""
if argv is None:
argv = sys.argv[1:]
# emulate old fslio/imtest - always return 0
if len(argv) != 1:
print('0')
return 0
if imtest(argv[0]):
print('1')
else:
print('0')
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# remove_ext.py - Remove file extensions from NIFTI image paths
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import sys
import fsl.utils.path as fslpath
usage = """Usage: remove_ext <list of image paths to remove extension from>
""".strip()
# This list is defined in the
# fsl.data.image class, but are duplicated
# here for performance (to avoid import of
# nibabel/numpy/etc).
exts = ['.nii.gz', '.nii',
'.img', '.hdr',
'.img.gz', '.hdr.gz',
'.mnc', '.mnc.gz']
"""List of file extensions that are removed by ``remove_ext``. """
def main(argv=None):
"""Removes file extensions from all paths which are specified on the
command line.
"""
if argv is None:
argv = sys.argv[1:]
if len(argv) < 1:
print(usage)
return 1
removed = []
for path in argv:
removed.append(fslpath.removeExt(path, exts))
print(' '.join(removed))
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python
#
# resample_image.py - Script to resample an image
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module defines the ``resample_image`` script, for resampling
a NIfTI image.
"""
import textwrap as tw
import sys
import argparse
import numpy as np
import fsl.utils.parse_data as parse_data
import fsl.utils.image.resample as resample
import fsl.data.image as fslimage
def intlist(val):
"""Turn a string of comma-separated ints into a list of ints. """
return [int(v) for v in val.split(',')]
def floatlist(val):
"""Turn a string of comma-separated floats into a list of floats. """
return [float(v) for v in val.split(',')]
def sanitiseList(parser, vals, img, arg):
"""Make sure that ``vals`` has the same number of elements as ``img`` has
dimensions. Used to sanitise the ``--shape`` and ``--dim`` options.
"""
if vals is None:
return vals
nvals = len(vals)
if nvals < 3:
parser.error('At least three values are '
'required for {}'.format(arg))
if nvals > img.ndim:
parser.error('Input only has {} dimensions - too many values '
'specified for {}'.format(img.ndim, arg))
if nvals < img.ndim:
vals = list(vals) + list(img.shape[nvals:])
return vals
ARGS = {
'input' : ('input',),
'output' : ('output',),
'shape' : ('-s', '--shape'),
'dim' : ('-d', '--dim'),
'reference' : ('-r', '--reference'),
'interp' : ('-i', '--interp'),
'origin' : ('-o', '--origin'),
'dtype' : ('-dt', '--dtype'),
'smooth' : ('-n', '--nosmooth')}
OPTS = {
'input' : dict(type=parse_data.Image),
'output' : dict(type=parse_data.ImageOut),
'reference' : dict(type=parse_data.Image, metavar='IMAGE'),
'shape' : dict(type=intlist, metavar=('X,Y,Z,...')),
'dim' : dict(type=floatlist, metavar=('X,Y,Z,...')),
'interp' : dict(choices=('nearest', 'linear', 'cubic'),
default='linear'),
'origin' : dict(choices=('centre', 'corner'), default='centre'),
'dtype' : dict(choices=('char', 'short', 'int', 'float', 'double')),
'smooth' : dict(dest='smooth', action='store_false')}
HELPS = {
'input' : 'Input image',
'output' : 'Output image',
'shape' : 'Output shape',
'dim' : 'Output voxel dimensions',
'reference' : 'Resample input to the space of this reference image'
'(overrides --origin)',
'interp' : 'Interpolation (default: linear)',
'origin' : 'Resampling origin (default: centre)',
'dtype' : 'Data type (default: data type of input image)',
'smooth' : 'Do not smooth image when downsampling'}
DESC = tw.dedent("""
Resample an image to different dimensions.
""").strip()
DEST_DESC = tw.dedent("""
Specify the resampling destination space using one of the following
options. Note that the --reference option will cause the field-of-view
of the input image to be changed to that of the reference image.
""").strip()
USAGE = 'resample_image (--shape|--dim|--reference) [options] input output'
INTERPS = {'nearest' : 0,
'linear' : 1,
'cubic' : 3}
DTYPES = {'char' : np.uint8,
'short' : np.int16,
'int' : np.int32,
'float' : np.float32,
'double' : np.float64}
def parseArgs(argv):
"""Parses command-line arguments.
:arg argv: Sequence of command-line arguments
:returns: An ``argparse.Namespace`` object containing parsed arguments.
"""
parser = argparse.ArgumentParser(prog='resample_image',
usage=USAGE,
description=DESC)
dest = parser.add_argument_group('Resampling destination', DEST_DESC)
dest = dest.add_mutually_exclusive_group(required=True)
for a in ('input', 'output', 'interp', 'origin',
'dtype', 'smooth'):
parser.add_argument(*ARGS[a], help=HELPS[a], **OPTS[a])
for a in ('shape', 'dim', 'reference'):
dest.add_argument(*ARGS[a], help=HELPS[a], **OPTS[a])
if len(argv) == 0:
parser.print_help()
sys.exit(0)
args = parser.parse_args(argv)
args.interp = INTERPS[ args.interp]
args.dtype = DTYPES.get(args.dtype, args.input.dtype)
args.shape = sanitiseList(parser, args.shape, args.input, 'shape')
args.dim = sanitiseList(parser, args.dim, args.input, 'dim')
if (args.reference is not None) and \
(args.input.ndim > 3) and \
(args.reference.ndim > 3):
print('Reference and image are both >3D - only '
'resampling along the spatial dimensions.')
return args
def main(argv=None):
"""Entry point for ``resample_image``. Parses arguments, resamples the
input image, and saves it to the specified output file.
:arg argv: Sequence of command-line arguments. If not provided, taken
from ``sys.argv``.
"""
if argv is None:
argv = sys.argv[1:]
args = parseArgs(argv)
reskwargs = {
'dtype' : args.dtype,
'order' : args.interp,
'smooth' : args.smooth,
'origin' : args.origin}
# One of these is guaranteed to be set
if args.shape is not None:
func = resample.resample
resargs = (args.input, args.shape)
elif args.dim is not None:
func = resample.resampleToPixdims
resargs = (args.input, args.dim)
elif args.reference is not None:
func = resample.resampleToReference
resargs = (args.input, args.reference)
resampled, xform = func(*resargs, **reskwargs)
if args.reference is None:
hdr = args.input.header
else:
hdr = args.reference.header
xform = None
resampled = fslimage.Image(resampled, xform=xform, header=hdr)
# Adjust the pixdims of the
# higher dimensions if they
# have been resampled
if len(resampled.shape) > 3:
oldPixdim = args.input.pixdim[3:]
oldShape = args.input.shape[ 3:]
newShape = resampled .shape[ 3:]
for i, (p, o, n) in enumerate(zip(oldPixdim, oldShape, newShape), 4):
resampled.header['pixdim'][i] = p * o / n
resampled.save(args.output)
return 0
if __name__ == '__main__':
sys.exit(main())
......@@ -10,6 +10,7 @@
import os
import sys
import glob
import hashlib
import shutil
import fnmatch
import logging
......@@ -20,14 +21,12 @@ import os.path as op
import numpy as np
import nibabel as nib
from six import StringIO
from io import StringIO
try: from unittest import mock
except ImportError: import mock
from unittest import mock
import fsl.data.image as fslimage
from fsl.utils.tempdir import tempdir
from fsl.utils.tempdir import tempdir
from fsl.utils.platform import platform as fslplatform
......@@ -35,7 +34,7 @@ logging.getLogger().setLevel(logging.WARNING)
@contextlib.contextmanager
def mockFSLDIR():
def mockFSLDIR(**kwargs):
oldfsldir = fslplatform.fsldir
oldfsldevdir = fslplatform.fsldevdir
......@@ -45,6 +44,15 @@ def mockFSLDIR():
fsldir = op.join(td, 'fsl')
bindir = op.join(fsldir, 'bin')
os.makedirs(bindir)
for subdir, files in kwargs.items():
subdir = op.join(fsldir, subdir)
if not op.isdir(subdir):
os.makedirs(subdir)
for fname in files:
fname = op.join(subdir, fname)
touch(fname)
if subdir == bindir:
os.chmod(fname, 0o755)
fslplatform.fsldir = fsldir
fslplatform.fsldevdir = None
......@@ -62,7 +70,7 @@ def touch(fname):
pass
class CaptureStdout(object):
class CaptureStdout:
"""Context manager which captures stdout and stderr. """
def __init__(self):
......@@ -81,6 +89,7 @@ class CaptureStdout(object):
sys.stdout = self.__mock_stdout
sys.stderr = self.__mock_stderr
return self
def __exit__(self, *args, **kwargs):
......@@ -139,6 +148,8 @@ def testdir(contents=None, suffix=""):
shutil.rmtree(self.testdir)
return ctx(contents)
testdir.__test__ = False
def make_dummy_files(paths):
"""Creates dummy files for all of the given paths. """
......@@ -278,7 +289,8 @@ def make_mock_feat_analysis(featdir,
copes=True,
zstats=True,
residuals=True,
clustMasks=True):
clustMasks=True,
zfstats=True):
if xform is None:
xform = np.eye(4)
......@@ -311,6 +323,7 @@ def make_mock_feat_analysis(featdir,
data = np.ravel_multi_index(data, shape)
data = data.reshape(list(shape) + [1]).repeat(timepoints, axis=3)
data[..., :] += range(i, i + timepoints)
data = data.astype(np.int32)
img = nib.nifti1.Nifti1Image(data, xform)
......@@ -335,6 +348,11 @@ def make_mock_feat_analysis(featdir,
otherFiles .extend(files)
otherShapes.extend([shape] * len(files))
if zfstats:
files = glob.glob(op.join(featdir, 'stats', 'zfstat*nii.gz'))
otherFiles .extend(files)
otherShapes.extend([shape] * len(files))
if residuals:
files = glob.glob(op.join(featdir, 'stats', 'res4d.nii.gz'))
otherFiles .extend(files)
......@@ -352,6 +370,56 @@ def make_mock_feat_analysis(featdir,
return featdir
def make_mock_melodic_analysis(basedir, shape4D, ntimepoints, xform=None):
if xform is None:
xform = np.eye(4)
ncomps = shape4D[-1]
halftime = int(np.floor(ntimepoints / 2))
os.makedirs(basedir)
make_random_image(op.join(basedir, 'melodic_IC.nii.gz'),
dims=shape4D,
xform=xform)
mix = np.random.randint(1, 255, (ntimepoints, ncomps))
ftmix = np.random.randint(1, 255, (halftime, ncomps))
np.savetxt(op.join(basedir, 'melodic_mix'), mix)
np.savetxt(op.join(basedir, 'melodic_FTmix'), ftmix)
def make_mock_dtifit_analysis(basedir, shape3D, basename='dti', xform=None, tensor=False):
if xform is None:
xform = np.eye(4)
os.makedirs(basedir)
shape4D = tuple(shape3D) + (3,)
def mk(ident, shp):
make_random_image(
op.join(basedir, '{}_{}.nii.gz'.format(basename, ident)),
shp,
xform)
mk('V1', shape4D)
mk('V2', shape4D)
mk('V3', shape4D)
mk('L1', shape3D)
mk('L2', shape3D)
mk('L3', shape3D)
mk('S0', shape3D)
mk('MD', shape3D)
mk('MO', shape3D)
mk('FA', shape3D)
if tensor:
mk('tensor', tuple(shape3D) + (6,))
def make_random_mask(filename, shape, xform, premask=None, minones=1):
"""Make a random binary mask image. """
......@@ -372,3 +440,10 @@ def make_random_mask(filename, shape, xform, premask=None, minones=1):
img.save(filename)
return img
def sha256(filename):
hashobj = hashlib.sha256()
with open(filename, 'rb') as f:
hashobj.update(f.read())
return hashobj.hexdigest()
......@@ -14,8 +14,8 @@ import pytest
import fsl.utils.assertions as assertions
import fsl.utils.tempdir as tempdir
from . import make_random_image
from . import testdir
from fsl.tests import make_random_image
from fsl.tests import testdir
def test_assertFileExists():
......@@ -160,14 +160,14 @@ def test_assertIsMelodicDir():
('analysis.ica', [ 'melodic_mix', 'melodic_FTmix'], False),
('analysis.ica', ['melodic_IC.nii.gz', 'melodic_FTmix'], False),
('analysis.ica', ['melodic_IC.nii.gz', 'melodic_mix'], False),
('analysis', ['melodic_IC.nii.gz', 'melodic_mix', 'melodic_FTmix'], False),
('analysis', ['melodic_oIC.nii.gz', 'melodic_mix', 'melodic_FTmix'], False),
('analysis', ['melodic_IC.nii.gz', 'melodic_mix', 'melodic_FTmix'], True),
('analysis', [ 'melodic_mix', 'melodic_FTmix'], False),
]
for dirname, paths, expected in tests:
with testdir(paths, dirname):
if expected:
assertions.assertIsMelodicDir(dirname)
assertions.assertIsMelodicDir('.')
else:
with pytest.raises(AssertionError):
assertions.assertIsMelodicDir(dirname)
......
......@@ -13,13 +13,15 @@ import os
import os.path as op
import numpy as np
import mock
from unittest import mock
import pytest
import tests
import fsl.utils.transform as transform
import fsl.data.atlases as atlases
import fsl.data.image as fslimage
import fsl.tests as tests
import fsl.utils.image.resample as resample
import fsl.data.atlases as atlases
import fsl.data.image as fslimage
import fsl.transform.affine as affine
datadir = op.join(op.dirname(__file__), 'testdata')
......@@ -39,7 +41,8 @@ dummy_atlas_desc = """<?xml version="1.0" encoding="ISO-8859-1"?>
<header>
<name>{name}</name>
<shortname>{shortname}</shortname>
<type>Label</type>
<type>{atlastype}</type>
{extraheader}
<images>
<imagefile>/{shortname}/{filename}</imagefile>
<summaryimagefile>/{shortname}/My{filename}</summaryimagefile>
......@@ -51,7 +54,8 @@ dummy_atlas_desc = """<?xml version="1.0" encoding="ISO-8859-1"?>
</data>
</atlas>
"""
def _make_dummy_atlas(savedir, name, shortName, filename):
def _make_dummy_atlas(
savedir, name, shortName, filename, atlastype='Label', extraheader=''):
mladir = op.join(savedir, shortName)
mlaxmlfile = op.join(savedir, '{}.xml'.format(shortName))
mlaimgfile = op.join(savedir, shortName, '{}.nii.gz'.format(filename))
......@@ -69,7 +73,9 @@ def _make_dummy_atlas(savedir, name, shortName, filename):
desc = dummy_atlas_desc.format(
name=name,
shortname=shortName,
filename=filename)
filename=filename,
atlastype=atlastype,
extraheader=extraheader)
f.write(desc)
return mlaxmlfile
......@@ -98,6 +104,8 @@ def test_AtlasDescription():
tal = registry.getAtlasDescription('talairach')
cort = registry.getAtlasDescription('harvardoxford-cortical')
assert str(tal) == 'AtlasDescription(talairach)'
assert str(cort) == 'AtlasDescription(harvardoxford-cortical)'
assert tal.atlasID == 'talairach'
assert tal.name == 'Talairach Daemon Labels'
......@@ -139,6 +147,26 @@ def test_AtlasDescription():
registry.getAtlasDescription('non-existent-atlas')
def test_StatisticHeader():
with tests.testdir() as testdir:
hdr = '<statistic>T</statistic>' \
'<units></units>' \
'<precision>3</precision>' \
'<upper>75</upper>'
xmlfile = _make_dummy_atlas(testdir,
'statlas',
'STA',
'StAtlas',
atlastype='Statistic',
extraheader=hdr)
desc = atlases.AtlasDescription(xmlfile, 'StAtlas')
assert desc.atlasType == 'statistic'
assert desc.statistic == 'T'
assert desc.units == ''
assert desc.precision == 3
assert desc.lower == 0
assert desc.upper == 75
def test_add_remove_atlas():
......@@ -224,8 +252,7 @@ def test_load_atlas():
reg = atlases.registry
reg.rescanAtlases()
probatlas = reg.loadAtlas('harvardoxford-cortical',
indexed=True, calcRange=False, loadData=False)
probatlas = reg.loadAtlas('harvardoxford-cortical')
probsumatlas = reg.loadAtlas('harvardoxford-cortical', loadSummary=True)
lblatlas = reg.loadAtlas('talairach')
......@@ -234,13 +261,32 @@ def test_load_atlas():
assert isinstance(lblatlas, atlases.LabelAtlas)
def test_get():
reg = atlases.registry
reg.rescanAtlases()
probatlas = reg.loadAtlas('harvardoxford-cortical')
lblatlas = reg.loadAtlas('talairach')
for atlas in (probatlas, lblatlas):
for idx, label in enumerate(atlas.desc.labels[:10]):
target = probatlas[..., idx] if atlas is probatlas else lblatlas.data == label.value
assert (target == atlas.get(label).data).all()
assert label.name == atlas.get(label).name
assert (target == atlas.get(index=label.index).data).all()
assert (target == atlas.get(value=label.value).data).all()
assert (target == atlas.get(name=label.name).data).all()
if atlas is lblatlas:
target = target * label.value
assert (target == atlas.get(value=label.value, binary=False).data).all()
def test_find():
reg = atlases.registry
reg.rescanAtlases()
probatlas = reg.loadAtlas('harvardoxford-cortical',
indexed=True, calcRange=False, loadData=False)
probatlas = reg.loadAtlas('harvardoxford-cortical')
probsumatlas = reg.loadAtlas('harvardoxford-cortical', loadSummary=True)
lblatlas = reg.loadAtlas('talairach')
......@@ -251,16 +297,31 @@ def test_find():
assert atlas .find(value=label.value) == label
assert atlas .find(index=label.index) == label
assert atlas .find(name=label.name) == label
assert atlas.desc.find(value=label.value) == label
assert atlas.desc.find(index=label.index) == label
assert atlas.desc.find(name=label.name) == label
if atlas is not lblatlas:
# lblatlas has a lot of very similar label names
assert atlas .find(name=label.name[:-2]) == label
assert atlas.desc.find(name=label.name[:-2]) == label
with pytest.raises(ValueError):
atlas.find()
with pytest.raises(ValueError):
atlas.find(index=1, value=1)
with pytest.raises(ValueError):
atlas.find(index=1, name=1)
with pytest.raises(ValueError):
atlas.find(value=1, name=1)
with pytest.raises(IndexError):
atlas.find(index=len(labels))
with pytest.raises(IndexError):
atlas.find(name='InvalidROI')
with pytest.raises(IndexError):
atlas.find(name='')
maxval = max([l.value for l in labels])
with pytest.raises(KeyError):
......@@ -272,8 +333,7 @@ def test_prepareMask():
reg = atlases.registry
reg.rescanAtlases()
probatlas = reg.loadAtlas('harvardoxford-cortical',
indexed=True, loadData=False, calcRange=False)
probatlas = reg.loadAtlas('harvardoxford-cortical')
probsumatlas = reg.loadAtlas('harvardoxford-cortical', loadSummary=True)
lblatlas = reg.loadAtlas('talairach')
......@@ -286,14 +346,14 @@ def test_prepareMask():
np.array(np.random.random(ashape), dtype=np.float32),
xform=atlas.voxToWorldMat)
goodmask2, xf = goodmask1.resample(m2shape)
goodmask2, xf = resample.resample(goodmask1, m2shape)
goodmask2 = fslimage.Image(goodmask2, xform=xf)
wrongdims = fslimage.Image(
np.random.random(list(ashape) + [2]))
wrongspace = fslimage.Image(
np.random.random((20, 20, 20)),
xform=transform.concat(atlas.voxToWorldMat, np.diag([2, 2, 2, 1])))
xform=affine.concat(atlas.voxToWorldMat, np.diag([2, 2, 2, 1])))
with pytest.raises(atlases.MaskError):
atlas.prepareMask(wrongdims)
......