Commit 5bf216bc authored by Paul McCarthy's avatar Paul McCarthy
Browse files

imcp/immv moved into their own module. They now have an option to honour

the default file extensions (i.e. the FSLOUTPUTTYPE) - this is used by
the fslpy_immv/imcp command line scripts.
parent 4ea48533
...@@ -11,20 +11,8 @@ from __future__ import print_function ...@@ -11,20 +11,8 @@ from __future__ import print_function
import os.path as op import os.path as op
import sys import sys
import fsl.utils.path as fslpath import fsl.utils.path as fslpath
import fsl.utils.imcp as imcp
import fsl.data.image as fslimage
SUPPORTED_EXTENSIONS = [
'.nii', '.nii.gz',
'.hdr', '.hdr.gz',
'.img', '.img.gz',
'.mnc', '.mnc.gz'
]
FILE_GROUPS = [
('.img', '.hdr'),
('.img.gz', '.hdr.gz')
]
usage = """Usage: usage = """Usage:
...@@ -37,35 +25,32 @@ Copy images from <file1> to <file2>, or copy all <file>s to <directory> ...@@ -37,35 +25,32 @@ Copy images from <file1> to <file2>, or copy all <file>s to <directory>
NB: filenames can be basenames or include an extension. NB: filenames can be basenames or include an extension.
Recognised file extensions: {} Recognised file extensions: {}
""".format(', '.join(SUPPORTED_EXTENSIONS)) """.format(', '.join(fslimage.ALLOWED_EXTENSIONS))
def main(): def main(argv=None):
"""Parses CLI arguments (see the usage string), and calls the """Parses CLI arguments (see the usage string), and calls the
fsl.utils.path.imcp function on each input. :func:`fsl.utils.imcp.imcp` function on each input.
""" """
if len(sys.argv) < 2: if len(argv) < 2:
print(usage) print(usage)
sys.exit(1) sys.exit(1)
srcs = sys.argv[1:-1] srcs = argv[:-1]
dest = sys.argv[ -1] dest = argv[ -1]
if len(srcs) > 1 and not op.isdir(dest): if len(srcs) > 1 and not op.isdir(dest):
print(usage) print(usage)
sys.exit(1) sys.exit(1)
srcs = fslpath.removeDuplicates(srcs, srcs = fslpath.removeDuplicates(srcs,
allowedExts=SUPPORTED_EXTENSIONS, allowedExts=fslimage.ALLOWED_EXTENSIONS,
fileGroups=FILE_GROUPS) fileGroups=fslimage.FILE_GROUPS)
for src in srcs: for src in srcs:
try: try:
fslpath.imcp(src, imcp.imcp(src, dest, useDefaultExt=True)
dest,
allowedExts=SUPPORTED_EXTENSIONS,
fileGroups=FILE_GROUPS)
except Exception as e: except Exception as e:
print(e) print(e)
......
...@@ -11,21 +11,8 @@ from __future__ import print_function ...@@ -11,21 +11,8 @@ from __future__ import print_function
import os.path as op import os.path as op
import sys import sys
import fsl.utils.path as fslpath import fsl.utils.path as fslpath
import fsl.utils.imcp as imcp
import fsl.data.image as fslimage
SUPPORTED_EXTENSIONS = [
'.nii', '.nii.gz',
'.hdr', '.hdr.gz',
'.img', '.img.gz',
'.mnc', '.mnc.gz'
]
FILE_GROUPS = [
('.img', '.hdr'),
('.img.gz', '.hdr.gz')
]
usage = """Usage: usage = """Usage:
...@@ -38,35 +25,35 @@ Moves images from <file1> to <file2>, or move all <file>s to <directory> ...@@ -38,35 +25,35 @@ Moves images from <file1> to <file2>, or move all <file>s to <directory>
NB: filenames can be basenames or include an extension. NB: filenames can be basenames or include an extension.
Recognised file extensions: {} Recognised file extensions: {}
""".format(', '.join(SUPPORTED_EXTENSIONS)) """.format(', '.join(fslimage.ALLOWED_EXTENSIONS))
def main(): def main(argv=None):
"""Parses CLI arguments (see the usage string), and calls the """Parses CLI arguments (see the usage string), and calls the
fsl.utils.path.immv function on each input. fsl.utils.imcp.immv function on each input.
""" """
if argv is None:
argv = sys.argv[1:]
if len(sys.argv) < 2: if len(argv) < 2:
print(usage) print(usage)
sys.exit(1) sys.exit(1)
srcs = sys.argv[1:-1] srcs = argv[:-1]
dest = sys.argv[ -1] dest = argv[ -1]
if len(srcs) > 1 and not op.isdir(dest): if len(srcs) > 1 and not op.isdir(dest):
print(usage) print(usage)
sys.exit(1) sys.exit(1)
srcs = fslpath.removeDuplicates(srcs, srcs = fslpath.removeDuplicates(srcs,
allowedExts=SUPPORTED_EXTENSIONS, allowedExts=fslimage.ALLOWED_EXTENSIONS,
fileGroups=FILE_GROUPS) fileGroups=fslimage.FILE_GROUPS)
for src in srcs: for src in srcs:
try: try:
fslpath.immv(src, imcp.immv(src, dest, useDefaultExt=True, move=True)
dest,
allowedExts=SUPPORTED_EXTENSIONS,
fileGroups=FILE_GROUPS)
except Exception as e: except Exception as e:
print(e) print(e)
......
...@@ -25,9 +25,10 @@ and file names: ...@@ -25,9 +25,10 @@ and file names:
:nosignatures: :nosignatures:
looksLikeImage looksLikeImage
removeExt addExt
splitExt splitExt
addExt getExt
removeExt
defaultExt defaultExt
loadIndexedImageFile loadIndexedImageFile
""" """
...@@ -789,36 +790,36 @@ def looksLikeImage(filename, allowedExts=None): ...@@ -789,36 +790,36 @@ def looksLikeImage(filename, allowedExts=None):
return any([filename.endswith(ext) for ext in allowedExts]) return any([filename.endswith(ext) for ext in allowedExts])
def removeExt(filename): def addExt(prefix, mustExist=True):
"""Removes the extension from the given file name. Returns the filename """Adds a file extension to the given file ``prefix``. See
unmodified if it does not have a supported extension. :func:`~fsl.utils.path.addExt`.
See :func:`~fsl.utils.path.removeExt`.
:arg filename: The file name to strip.
""" """
return fslpath.removeExt(filename, ALLOWED_EXTENSIONS) return fslpath.addExt(prefix,
ALLOWED_EXTENSIONS,
mustExist,
defaultExt(),
fileGroups=FILE_GROUPS)
def splitExt(filename): def splitExt(filename):
"""Splits the base name and extension for the given ``filename``. """Splits the base name and extension for the given ``filename``. See
:func:`~fsl.utils.path.splitExt`.
See :func:`~fsl.utils.path.splitExt`.
""" """
return fslpath.splitExt(filename, ALLOWED_EXTENSIONS) return fslpath.splitExt(filename, ALLOWED_EXTENSIONS)
def addExt(prefix, mustExist=True): def getExt(filename):
"""Adds a file extension to the given file ``prefix``. """Gets the extension for the given file name. See
:func:`~fsl.utils.path.getExt`.
"""
return fslpath.getExt(filename, ALLOWED_EXTENSIONS)
See :func:`~fsl.utils.path.addExt`. def removeExt(filename):
"""Removes the extension from the given file name. See
:func:`~fsl.utils.path.removeExt`.
""" """
return fslpath.addExt(prefix, return fslpath.removeExt(filename, ALLOWED_EXTENSIONS)
ALLOWED_EXTENSIONS,
mustExist,
defaultExt(),
fileGroups=FILE_GROUPS)
def defaultExt(): def defaultExt():
......
#!/usr/bin/env python
#
# imcp.py - Functions for moving/copying NIFTI image files.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""
.. autosummary::
:nosignatures:
imcp
immv
"""
import os
import os.path as op
import shutil
import fsl.utils.path as fslpath
import fsl.data.image as fslimage
def imcp(src,
dest,
overwrite=False,
useDefaultExt=False,
move=False):
"""Copy the given ``src`` file to destination ``dest``.
A :class:`.fsl.utils.path.PathError` is raised if anything goes wrong.
:arg src: Path to copy. If ``allowedExts`` is provided,
the file extension can be omitted.
:arg dest: Destination path. Can be an incomplete file
specification (i.e. without the extension), or a
directory.
:arg overwrite: If ``True`` this function will overwrite files that
already exist. Defaults to ``False``.
:arg useDefaultExt: Defaults to ``False``. If ``True``, the destination
file type will be set according to the default
extension, specified by
:func:`~fsl.data.image.defaultExt`.
:arg move: If ``True``, the files are moved, instead of being
copied. See :func:`immv`.
"""
import nibabel as nib
if op.isdir(dest):
dest = op.join(dest, op.basename(src))
srcBase, srcExt = fslimage.splitExt(src)
destBase, destExt = fslimage.splitExt(dest)
# src was specified without an
# extension, or the specified
# src does not have an allowed
# extension.
if srcExt == '':
# Try to resolve the specified src
# path - if src does not exist, or
# does not have an allowed extension,
# addExt will raise an error
src = fslimage.addExt(src, mustExist=True)
# We've resolved src to a
# full filename - split it
# again to get its extension
srcBase, srcExt = fslimage.splitExt(src)
# Figure out the destination file
# extension/type. If useDefaultExt
# is True, we use the default
# extension. Otherwise, if no
# destination file extension is
# provided, we use the source
# extension.
if useDefaultExt: destExt = fslimage.defaultExt()
elif destExt == '': destExt = srcExt
# Resolve any file group differences
# e.g. we don't care if the src is
# specified as file.hdr, and the dest
# is specified as file.img - if src
# and dest are part of the same file
# group, we replace the dest extension
# with the src extension.
if srcExt != destExt:
for group in fslimage.FILE_GROUPS:
if srcExt in group and destExt in group:
destExt = srcExt
break
dest = destBase + destExt
# Give up if we don't have permission.
if not os.access(op.dirname(dest), os.W_OK | os.X_OK):
raise fslpath.PathError('imcp error - cannot write to {}'.format(dest))
if move and not os.access(op.dirname(src), os.W_OK | os.X_OK):
raise fslpath.PathError('imcp error - cannot move from {}'.format(src))
# If the source file type does not
# match the destination file type,
# we need to perform a conversion.
#
# This is expensive in terms of io
# and cpu, but programmatically
# very easy - nibabel does all the
# hard work.
if srcExt != destExt:
if not overwrite and op.exists(dest):
raise fslpath.PathError('imcp error - destination already '
'exists ({})'.format(dest))
img = nib.load(src)
nib.save(img, dest)
if move:
os.remove(src)
return
# Otherwise we do a file copy. This
# is actually more complicated than
# conveting the file type due to
# hdr/img pairs ...
#
# If the source is part of a file group,
# e.g. src.img/src.hdr), we need to copy
# the whole set of files. So here we
# build a list of source files that need
# to be copied/moved. The getFileGroup
# function returns all other files that
# are associated with this file (i.e.
# part of the same group).
#
# We store the sources as separate
# (base, ext) tuples, so we don't
# have to re-split when creating
# destination paths.
copySrcs = fslpath.getFileGroup(src,
fslimage.ALLOWED_EXTENSIONS,
fslimage.FILE_GROUPS,
fullPaths=False)
copySrcs = [(srcBase, e) for e in copySrcs]
# Note that these additional files
# do not have to exist, e.g.
# imcp('blah.img', ...) will still
# work if there is no blah.hdr
copySrcs = [(b, e) for (b, e) in copySrcs if op.exists(b + e)]
# Build a list of destinations for each
# copy source - we build this list in
# advance, so we can fail if any of the
# destinations already exist. We also
# re-combine the source bases/extensions.
copyDests = [destBase + e for (b, e) in copySrcs]
copySrcs = [b + e for (b, e) in copySrcs]
# Fail if any of the destination
# paths already exist
if not overwrite and any([op.exists(d) for d in copyDests]):
raise fslpath.PathError('imcp error - a destination path already '
'exists ({})'.format(', '.join(copyDests)))
# Do the copy/move
for src, dest in zip(copySrcs, copyDests):
if move: shutil.move(src, dest)
else: shutil.copy(src, dest)
def immv(src,
dest,
overwrite=False,
useDefaultExt=True):
"""Move the specified ``src`` to the specified ``dest``. See :func:`imcp`.
"""
imcp(src,
dest,
overwrite=overwrite,
useDefaultExt=useDefaultExt,
move=True)
...@@ -18,13 +18,10 @@ paths. ...@@ -18,13 +18,10 @@ paths.
getExt getExt
splitExt splitExt
getFileGroup getFileGroup
imcp
immv
""" """
import os.path as op import os.path as op
import shutil
class PathError(Exception): class PathError(Exception):
...@@ -231,23 +228,23 @@ def removeDuplicates(paths, allowedExts=None, fileGroups=None): ...@@ -231,23 +228,23 @@ def removeDuplicates(paths, allowedExts=None, fileGroups=None):
001.img 001.img
002.hdr 002.hdr
002.img 002.img
003.img 003.hdr
003.img 003.img
And you call ``removeDuplicates`` like so:: And you call ``removeDuplicates`` like so::
paths = ['001.img', '001.hdr', paths = ['001.img', '001.hdr',
'002.img', '002.hdr', '002.img', '002.hdr',
'003.img', '003.hdr'] '003.img', '003.hdr']
allowedExts = ['.img', '.hdr'] allowedExts = ['.img', '.hdr']
fileGroups = [('.img', '.hdr')] fileGroups = [('.img', '.hdr')]
removeDuplicates(paths, allowedExts, fileGroups) removeDuplicates(paths, allowedExts, fileGroups)
The returned list will be:: The returned list will be::
['001.img', '003.img', '003.img'] ['001.img', '002.img', '003.img']
:arg paths: List of paths to reduce. :arg paths: List of paths to reduce.
...@@ -290,16 +287,17 @@ def getFileGroup(path, allowedExts=None, fileGroups=None, fullPaths=True): ...@@ -290,16 +287,17 @@ def getFileGroup(path, allowedExts=None, fileGroups=None, fullPaths=True):
return ``file.img`` (i.e. the file which matches the first extension in return ``file.img`` (i.e. the file which matches the first extension in
the group). the group).
Similarly, if you call the :func:`imcp` or :func:`immv` functions with the Similarly, if you call the :func:`.imcp.imcp` or :func:`.imcp.immv`
above parameters, both ``file.img`` and ``file.hdr`` will be moved. functions with the above parameters, both ``file.img`` and ``file.hdr``
will be moved.
.. note:: The primary use-case of file groups is to resolve ambiguity with .. note:: The primary use-case of file groups is to resolve ambiguity with
respect to NIFTI and ANALYSE75 image pairs. By specifying respect to NIFTI and ANALYSE75 image pairs. By specifying
``fileGroups=[('.img', '.hdr'), ('.img.gz', '.hdr.gz')]``, the ``fileGroups=[('.img', '.hdr'), ('.img.gz', '.hdr.gz')]``, the
:func:`addExt`, :func:`immv` and :func:`imcp` functions are able :func:`addExt`, :func:`.imcp.immv` and :func:`.imcp.imcp`
to figure out what you mean when you specify ``file``, and both functions are able to figure out what you mean when you specify
``file.hdr`` and ``file.img`` (or ``file.hdr.gz`` and ``file``, and both ``file.hdr`` and ``file.img`` (or
``file.img.gz``) exist. ``file.hdr.gz`` and ``file.img.gz``) exist.
:arg path: Path to the file. Must contain the file extension. :arg path: Path to the file. Must contain the file extension.
...@@ -342,116 +340,3 @@ def getFileGroup(path, allowedExts=None, fileGroups=None, fullPaths=True): ...@@ -342,116 +340,3 @@ def getFileGroup(path, allowedExts=None, fileGroups=None, fullPaths=True):
else: else:
if fullPaths: return matchedGroupFiles[0] if fullPaths: return matchedGroupFiles[0]
else: return matchedGroups[ 0] else: return matchedGroups[ 0]
def imcp(src,
dest,
allowedExts=None,
fileGroups=None,
overwrite=False,
move=False):
"""Copy the given ``src`` file to destination ``dest``.
:arg src: Path to copy. If ``allowedExts`` is provided,
the file extension can be omitted.
:arg dest: Destination path. Can be an incomplete file
specification (i.e. without the extension), or a
directory.
:arg allowedExts: Allowed/recognised file extensions.
:arg fileGroups: Recognised file groups - see the :func:`getFileGroup`
documentation.
:arg overwrite: If ``True`` this function will overwrite files that
already exist. Defaults to ``False``.
:arg move: If ``True``, the files are moved, instead of being
copied.
"""
base, ext = splitExt(src, allowedExts)
destIsDir = op.isdir(dest)
# If dest has been specified
# as a file name, we don't
# care about its extension.
if not destIsDir:
dest = removeExt(dest, allowedExts)
# src was specified without an
# extension, or the specitifed
# src does not have an allowed
# extension.
if ext == '':
# Try to resolve the specified src
# path - if src does not exist, or
# does not have an allowed extension,
# addExt will raise an error
src = addExt(src,
allowedExts,
mustExist=True,
fileGroups=fileGroups)
# We've resolved src to a
# full filename - split it
# again to get its extension
base, ext = splitExt(src, allowedExts)
# If the source is part of a file group,
# e.g. src.img/src.hdr, we want to copy
# the whole set of files. So here we
# build a list of source files that need
# to be copied/moved. The getFileGroup
# function returns all other files that
# are associated with this file (i.e.
# part of the same group).
#
# We store the sources as separate
# (base, ext) tuples, so we don't
# have to re-split when creating
# destination paths.
copySrcs = getFileGroup(src, allowedExts, fileGroups, fullPaths=False)
copySrcs = [(base, e) for e in copySrcs]
# Note that these additional files
# do not have to exist, e.g.
# imcp('blah.img', ...) will still
# work if there is no blah.hdr
copySrcs = [(b, e) for (b, e) in copySrcs if op.exists(b + e)]
# Build a list of destinations for each
# copy source - we build this list in