#
# loadoverlay.py - Action which allows the user to load overlay files.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`LoadOverlayAction`, which allows the user
to load overlay files into the :class:`.OverlayList`.
This module also provides a collection of standalone functions which can be
called directly:
.. autosummary::
:nosignatures:
makeWildcard
loadOverlays
loadImage
interactiveLoadOverlays
Finally, this module provides a singleton :class:`RecentPathManager` instance
called :attr:`recentPathManager`, which can be registered with to be notified
when new files have been loaded.
"""
import logging
import os
import os.path as op
import numpy as np
import fsl.utils.idle as idle
import fsl.utils.notifier as notifier
import fsl.utils.settings as fslsettings
import fsl.data.utils as dutils
import fsleyes_widgets.utils.status as status
import fsleyes.autodisplay as autodisplay
import fsleyes.strings as strings
from . import base
log = logging.getLogger(__name__)
[docs]class LoadOverlayAction(base.Action):
"""The ``LoadOverlayAction`` allows the user to add files to the
:class:`.OverlayList`.
"""
[docs] def __init__(self, overlayList, displayCtx, frame):
"""Create a ``LoadOverlayAction``.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`.
:arg frame: The :class:`.FSLeyesFrame`.
"""
base.Action.__init__(self, overlayList, displayCtx, self.__loadOverlay)
def __loadOverlay(self):
"""Calls :func:`interactiveLoadOverlays`.
If overlays were added, updates the
:attr:`.DisplayContext.selectedOverlay` accordingly.
If :attr:`.DisplayContext.autoDisplay` is ``True``, uses the
:mod:`.autodisplay` module to configure the display properties
of each new overlay.
"""
def onLoad(paths, overlays):
if len(overlays) == 0:
return
self.overlayList.extend(overlays)
self.displayCtx.selectedOverlay = self.displayCtx.overlayOrder[-1]
if self.displayCtx.autoDisplay:
for overlay in overlays:
autodisplay.autoDisplay(overlay,
self.overlayList,
self.displayCtx)
interactiveLoadOverlays(onLoad=onLoad,
inmem=self.displayCtx.loadInMemory)
[docs]def makeWildcard(allowedExts=None, descs=None):
"""Returns a wildcard string for use in a file dialog, to limit
the the displayed file types to supported overlay file types.
"""
import fsl.data.image as fslimage
import fsl.data.mghimage as fslmgh
import fsl.data.vtk as fslvtk
import fsl.data.gifti as fslgifti
import fsl.data.freesurfer as fslfs
import fsl.data.bitmap as fslbmp
# Hack - the wx wildcard logic doesn't support
# files with multiple extensions (e.g. .nii.gz).
# So I'm adding support for the '.gz' extension
# here.
fsfiles = [op.splitext(f)[1] for f in fslfs.CORE_GEOMETRY_FILES]
if allowedExts is None:
allowedExts = (fslimage.ALLOWED_EXTENSIONS +
fslvtk .ALLOWED_EXTENSIONS +
fslmgh .ALLOWED_EXTENSIONS +
fslgifti.ALLOWED_EXTENSIONS +
fslbmp .BITMAP_EXTENSIONS +
fsfiles +
['.gz'])
if descs is None:
descs = (fslimage.EXTENSION_DESCRIPTIONS +
fslvtk .EXTENSION_DESCRIPTIONS +
fslmgh .EXTENSION_DESCRIPTIONS +
fslgifti.EXTENSION_DESCRIPTIONS +
fslbmp .BITMAP_DESCRIPTIONS +
fslfs .CORE_GEOMETRY_DESCRIPTIONS +
['Compressed images'])
exts = ['*{}'.format(ext) for ext in allowedExts]
exts = [';'.join(exts)] + exts
descs = ['All supported files'] + descs
wcParts = ['|'.join((desc, ext)) for (desc, ext) in zip(descs, exts)]
return '|'.join(wcParts)
[docs]def loadOverlays(paths,
loadFunc='default',
errorFunc='default',
saveDir=True,
onLoad=None,
inmem=False,
blocking=False):
"""Loads all of the overlays specified in the sequence of files
contained in ``paths``.
.. note:: The overlays are loaded asynchronously via :func:`.idle.idle`.
Use the ``onLoad`` argument if you wish to be notified when
the overlays have been loaded.
:arg loadFunc: A function which is called just before each overlay
is loaded, and is passed the overlay path. The default
load function uses the :mod:`.status` module to display
the name of the overlay currently being loaded. Pass in
``None`` to disable this default behaviour.
:arg errorFunc: A function which is called if an error occurs while
loading an overlay, being passed the name of the
overlay, and either the :class:`Exception` which
occurred, or a string containing an error message. The
default function pops up a :class:`wx.MessageBox` with
an error message. Pass in ``None`` to disable this
default behaviour.
:arg saveDir: If ``True`` (the default), the directory of the last
overlay in the list of ``paths`` is saved, and used
later on as the default load directory.
:arg onLoad: Optional function to call when all overlays have been
loaded. Must accept two parameters:
- a list of indices, one for each overlay, into the
``paths`` parameter, indicating, for each overlay, the
path from which it was loaded.
- a list of the overlays that were loaded
:arg inmem: If ``True``, all :class:`.Image` overlays are
force-loaded into memory. Otherwise, large compressed
files may be kept on disk. Defaults to ``False``.
:arg blocking: Defaults to ``False``. If ``True``, overlays are loaded
immediately (and the ``onLoad`` function is called
directly. Otherwise, overlays and the ``onLoad`` are loaded
and called on the :func:`.idle.idle` loop.
:returns: If ``blocking is False`` (the default), returns ``None``.
Otherwise returns a list containing the loaded overlay
objects.
"""
import fsl.data.image as fslimage
import fsl.data.mesh as fslmesh
import fsl.data.bitmap as fslbmp
# The default load function updates
# the dialog window created above
def defaultLoadFunc(s):
msg = strings.messages['loadOverlays.loading'].format(s)
status.update(msg)
# The default error function
# shows an error dialog
def defaultErrorFunc(s, e):
status.reportError(
strings.titles[ 'loadOverlays.error'],
strings.messages['loadOverlays.error'].format(s),
e)
# A function which loads a single overlay
def loadPath(path, idx):
loadFunc(path)
dtype, path = dutils.guessType(path)
if dtype is None:
errorFunc(path, strings.messages['loadOverlays.unknownType'])
return
log.debug('Loading overlay {} (guessed data type: {})'.format(
path, dtype.__name__))
try:
if issubclass(dtype, fslimage.Image):
loaded = loadImage(dtype, path, inmem=inmem)
elif issubclass(dtype, fslmesh.Mesh):
loaded = [dtype(path, fixWinding=True)]
elif issubclass(dtype, fslbmp.Bitmap):
loaded = [dtype(path).asImage()]
else:
loaded = [dtype(path)]
overlays.extend(loaded)
pathIdxs.extend([idx] * len(loaded))
except Exception as e:
errorFunc(path, e)
# Record the path in the
# recent files list
recentPathManager.recordPath(path)
# This function gets called after
# all overlays have been loaded
def realOnLoad(*a):
if saveDir and len(paths) > 0:
ovlDir = op.abspath(op.dirname(paths[-1]))
fslsettings.write('loadSaveOverlayDir', ovlDir)
if onLoad is not None:
onLoad(pathIdxs, overlays)
# If loadFunc or errorFunc are explicitly set to
# None, use these no-op load/error functions
if loadFunc is None: loadFunc = lambda s: None
if errorFunc is None: errorFunc = lambda s, e: None
# Or if not provided, use the
# default functions defined above
if loadFunc == 'default': loadFunc = defaultLoadFunc
if errorFunc == 'default': errorFunc = defaultErrorFunc
pathIdxs = []
overlays = []
funcs = []
# Load the images
for idx, path in enumerate(paths):
funcs.append(lambda p=path, i=idx: loadPath(p, i))
funcs.append(realOnLoad)
for func in funcs:
if blocking: func()
else: idle.idle(func)
if blocking: return overlays
else: return None
[docs]def loadImage(dtype, path, inmem=False):
"""Called by the :func:`loadOverlays` function. Loads an overlay which
is represented by an ``Image`` instance, or a sub-class of ``Image``.
Depending upon the image size, the data may be loaded into memory or
kept on disk, and the initial image data range may be calculated
from the whole image, or from a sample.
This function returns a sequence, most likely containing a single
:class:`.Image` instance. But in some circumstances, more than one
:class:`.Image` will be created and returned.
:arg dtype: Overlay type (``Image``, or a sub-class of ``Image``).
:arg path: Path to the overlay file.
:arg inmem: If ``True``, ``Image`` overlays are loaded into memory.
:returns: A sequence of :class:`.Image` instances that were loaded.
"""
import fsl.data.image as fslimage
# We're going to load the file
# twice - first to get its
# dimensions/data type, and
# then for real.
#
# TODO It is annoying that you have to create a 'dtype'
# instance twice, as e.g. the MelodicImage does a
# bunch of extra stuff (e.g. loading component
# time courses) that don't need to be done. Maybe
# the path passed to this function could be resolved
# (e.g. ./filtered_func.ica/ turned into
# ./filtered_func.ica/melodic_IC) so that you can
# just create a fsl.data.Image, or a nib.Nifti1Image.
image = dtype(path,
loadData=False,
calcRange=False,
threaded=False)
nbytes = np.prod(image.shape) * image.dtype.itemsize
image = None
return [_loadImage(dtype, path, nbytes, inmem)]
[docs]def _loadImage(dtype, path, nbytes, inmem):
"""Loads an image with a non-complex data type.
:arg dtype: Overlay type - :class:`.Image`, or a sub-class of ``Image``.
:arg path: Path to the image file
:arg nbytes: Number of bytes that the image data takes up.
:arg inmem: If ``True``, the file is loaded into memory.
"""
# If the file is compressed (gzipped),
# tell the image to use a separate
# thread for data range calculation.
#
# The "idxthres" is so-named because
# it previously controlled whether
# gzipped images where kept on disk,
# and accessed via indexed_gzip. This
# is now determined automatically for
# us by nibabel.
rangethres = fslsettings.read('fsleyes.overlay.rangethres', 419430400)
idxthres = fslsettings.read('fsleyes.overlay.idxthres', 1073741824)
threaded = nbytes > idxthres
image = dtype(path,
loadData=inmem,
calcRange=False,
threaded=threaded,
loadMeta=True)
# If the image is bigger than the
# index threshold, keep it on disk.
if inmem or (not threaded):
log.debug('Loading {} into memory'.format(path))
image.loadData()
else:
log.debug('Keeping {} on disk'.format(path))
# If the image size is less than the range
# threshold, calculate the full data range
# now. Otherwise calculate the data range
# from a sample. This is handled by the
# Image.calcRange method.
image.calcRange(rangethres)
return image
[docs]def interactiveLoadOverlays(fromDir=None, dirdlg=False, **kwargs):
"""Convenience function for interactively loading one or more overlays.
Pops up a file dialog prompting the user to select one or more overlays
to load.
:arg fromDir: Directory in which the file dialog should start. If
``None``, the most recently visited directory (via this
function) is used, or a directory from An already loaded
overlay, or the current working directory.
:arg dirdlg: Use a directory chooser instead of a file dialog.
:arg kwargs: Passed through to the :func:`loadOverlays` function.
:raise ImportError: if :mod:`wx` is not present.
:raise RuntimeError: if a :class:`wx.App` has not been created.
"""
import wx
app = wx.GetApp()
if app is None:
raise RuntimeError('A wx.App has not been created')
saveFromDir = False
if fromDir is None:
saveFromDir = True
fromDir = fslsettings.read('loadSaveOverlayDir')
if fromDir is None:
fromDir = os.getcwd()
if dirdlg:
msg = strings.titles['interactiveLoadOverlays.dirDialog']
dlg = wx.DirDialog(app.GetTopWindow(),
message=msg,
defaultPath=fromDir,
style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST)
else:
msg = strings.titles['interactiveLoadOverlays.fileDialog']
dlg = wx.FileDialog(app.GetTopWindow(),
message=msg,
defaultDir=fromDir,
wildcard=makeWildcard(),
style=wx.FD_OPEN | wx.FD_MULTIPLE)
if dlg.ShowModal() != wx.ID_OK:
return
if dirdlg: paths = [dlg.GetPath()]
else: paths = dlg.GetPaths()
dlg.Close()
dlg.Destroy()
loadOverlays(paths, saveDir=saveFromDir, **kwargs)
[docs]class RecentPathManager(notifier.Notifier):
"""The ``RecentPathManager`` is a simple class which provides
access to a list of recently loaded files, and can notify
registered listeners when that list changes. See the
:attr:`recentPathManager` singleton instance.
"""
[docs] def recordPath(self, path):
"""Adds the given ``path`` to the recent files list. """
recent = self.listRecentPaths()
if path in recent:
return
recent.append(path)
if len(recent) > 10:
recent = recent[-10:]
recent = op.pathsep.join(recent)
fslsettings.write('fsleyes.recentFiles', recent)
self.notify()
[docs] def listRecentPaths(self):
"""Returns a list of recently loaded files. """
recent = fslsettings.read('fsleyes.recentFiles', None)
if recent is None: recent = []
else: recent = recent.split(op.pathsep)
return [f for f in recent if op.exists(f)]
recentPathManager = RecentPathManager()
"""A :class:`RecentPathManager` instance which gets updated by the
:func:`loadOverlays` function whenever a new path is loaded. Register
as a listener on this instance if you want to be notified of changes
to the recent paths list.
"""