#
# overlay.py - Defines the OverlayList class, and a few utility functions
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module defines the :class:`OverlayList` class, which is a simple but
fundamental class in *FSLeyes* - it is a container for all loaded overlays.
Only one ``OverlayList`` ever exists, and it is shared throughout the entire
application.
**What is an overlay?**
The definition of an *overlay* is fairly broad; any object can be added to
the ``OverlayList`` - there is no ``Overlay`` base class, nor any interface
which must be provided by an overlay object. The only requirements imposed on
an overlay type are:
- Must be able to be created with a single ``__init__`` parameter, which
is a string specifying the data source location (e.g. a file name) (but
see the note below about ``Image`` overlays).
- Must have an attribute called ``name``, which is used as the initial
display name for the overlay.
- Must have an attribute called ``dataSource``, which is used to identify
the source of the overlay data.
- Must be hashable (i.e. usable as a dictionary key).
- Must be supported by the :mod:`~fsleyes.gl` package .. ok, this is a
pretty big requirement .. See the :mod:`.globject` and the
:data:`.displaycontext.OVERLAY_TYPES` documentation for details on how to
get started with this one.
One further requirement is imposed on overlay types which derive from the
:class:`.Image` class:
- The ``__init__`` method for sub-classes of the ``Image`` class must
accept the ``loadData``, ``calcRange``, ``indexed`` , and ``threaded``
parameters, and pass them through to the base class ``__init__`` method.
Currently (``fsleyes`` version |version|) the only overlay types in existence
(and able to be rendered) are:
.. autosummary::
:nosignatures:
~fsl.data.image.Image
~fsl.data.featimage.FEATImage
~fsl.data.melodicimage.MelodicImage
~fsl.data.mghimage.MGHImage
~fsl.data.dtifit.DTIFitTensor
~fsl.data.vtk.VTKMesh
~fsl.data.gifti.GiftiMesh
~fsl.data.freesurfer.FreesurferMesh
~fsl.data.bitmap.Bitmap
This module also provides a few convenience classes and functions:
.. autosummary::
:nosignatures:
ProxyImage
findFEATImage
"""
import os.path as op
import logging
import weakref
import collections
import fsl.data.utils as dutils
import fsl.data.image as fslimage
import fsleyes_props as props
log = logging.getLogger(__name__)
[docs]class OverlayList(props.HasProperties):
"""Class representing a collection of overlays to be displayed together.
Contains a :class:`props.properties_types.List` property called
:attr:`overlays`, containing overlay objects (e.g. :class:`.Image` or
:class:`.Mesh` objects). Listeners can be registered on the
``overlays`` property, so they are notified when the overlay list changes.
An :class:`OverlayList` object has a few wrapper methods around the
:attr:`overlays` property, allowing the :class:`OverlayList` to be used as
if it were a list itself.
The :mod:`.loadoverlay` module contains some convenience functions for
loading and adding overlays.
The :meth:`getData` and :meth:`setData` methods allow arbitrary bits
of data associated with an overlay to be stored and retrieved.
"""
def __validateOverlay(self, atts, overlay):
"""Makes sure that the given overlay object is valid."""
return (hasattr(overlay, 'name') and
hasattr(overlay, 'dataSource'))
overlays = props.List(
listType=props.Object(allowInvalid=False,
validateFunc=__validateOverlay))
"""A list of overlay objects to be displayed."""
[docs] def __init__(self, overlays=None):
"""Create an ``OverlayList`` object from the given sequence of
overlays."""
if overlays is None: overlays = []
self.overlays.extend(overlays)
# The append/insert methods allow display/opts
# property values to be specified for newly
# added overlays. These can be queried via
# the initProps method (and is done so by
# DisplayContext/Display instances).
# It is a dict of dicts:
#
# {
# overlay : {
# propName: value,
# propName: value,
# },
# ...
# }
self.__initProps = collections.defaultdict(dict)
# This dictionary may be used throughout FSLeyes,
# via the getData/setData methods, to store
# any sort of data associated with an overlay.
# It is a dict of dicts:
#
# {
# overlay : {
# key : value,
# key : value,
# },
# overlay : {
# key : value,
# key : value,
# }
# }
self.__overlayData = weakref.WeakKeyDictionary()
[docs] def initProps(self, overlay):
"""Returns a dict containing initial :class:`.Display` and
:class:`.DisplayOpts` property values to be used for the given
``overlay``, if they were specified via the :meth:`append` or
:meth:`insert` methods.
This method requires that there is no overlap between the property
names used in :class:`.Display` and :class:`.DisplayOpts` classes.
"""
return self.__initProps[overlay]
[docs] def getData(self, overlay, key, *args):
"""Returns any stored value associated with the specified ``overlay``
and ``key``.
:arg default: Default value if there is no value associated with the
given ``key``. If not specified, and an unknown key is
given, a ``KeyError`` is raised.
"""
if len(args) not in (0, 1):
raise RuntimeError('Invalid arguments: {}'.format(args))
ovlDict = self.__overlayData.get(overlay, {})
if len(args) == 1:
return ovlDict.get(key, args[0])
else:
return ovlDict[key]
[docs] def setData(self, overlay, key, value):
"""Stores the given value via the specified ``overlay`` and ``key``.
"""
ovlDict = self.__overlayData.get(overlay, None)
if ovlDict is not None:
ovlDict[key] = value
else:
self.__overlayData[overlay] = {key : value}
[docs] def find(self, name):
"""Returns the first overlay with the given ``name`` or ``dataSource``,
or ``None`` if there is no overlay with said ``name``/``dataSource``.
"""
if name is None:
return None
absname = op.abspath(name)
for overlay in self.overlays:
if overlay.name == name:
return overlay
if overlay.dataSource is None:
continue
# Ignore file extensions for NIFTI images.
if isinstance(overlay, fslimage.Image):
if fslimage.removeExt(overlay.dataSource) == \
fslimage.removeExt(absname):
return overlay
else:
if overlay.dataSource == absname:
return overlay
return None
[docs] def __str__(self):
return self.overlays.__str__()
[docs] def __repr__(self):
return self.overlays.__str__()
# Wrappers around the overlays list property, allowing this
# OverlayList object to be used as if it is actually a list.
[docs] def __len__(self):
return self.overlays.__len__()
[docs] def __getitem__(self, key):
return self.overlays.__getitem__(key)
[docs] def __iter__(self):
return self.overlays.__iter__()
[docs] def __contains__(self, item):
return self.overlays.__contains__(item)
[docs] def __setitem__(self, key, val):
return self.overlays.__setitem__(key, val)
[docs] def __delitem__(self, key):
if isinstance(key, slice): pass
elif isinstance(key, int): key = slice(key, key + 1, None)
else: raise IndexError('Invalid key type')
ovls = self[key]
for ovl in ovls:
self.__initProps.pop(ovl, None)
return self.overlays.__delitem__(key)
[docs] def index(self, item):
return self.overlays.index(item)
[docs] def count(self, item):
return self.overlays.count(item)
[docs] def insert(self, index, item, **initProps):
"""Insert a new overlay into the overlay list.
Any initial :class:`.Display`/:class:`.DisplayOpts` property values
may be passed in as keyword arguments.
"""
with props.suppress(self, 'overlays', notify=True):
self.overlays.insert(index, item)
self.__initProps[item] = initProps
[docs] def append(self, item, **initProps):
"""Add a new overlay to the end of the overlay list.
Any initial :class:`.Display`/:class:`.DisplayOpts` property values
may be passed in as keyword arguments.
"""
self.insert(len(self), item, **initProps)
[docs] def extend(self, iterable, **initProps):
"""Add new overlays to the overlay list.
Any initial :class:`.Display`/:class:`.DisplayOpts` property values
may be passed in as keyword arguments, where the argument name is the
property name, and the argument value is a dict of
``{overlay : value}`` mappings.
"""
with props.suppress(self, 'overlays', notify=True):
result = self.overlays.extend(iterable)
for propName, overlayProps in initProps.items():
for overlay, val in overlayProps.items():
self.__initProps[overlay][propName] = val
return result
[docs] def insertAll(self, index, items):
return self.overlays.insertAll(index, items)
[docs] def pop(self, index=-1):
ovl = self.overlays.pop(index)
self.__initProps.pop(ovl, None)
return ovl
[docs] def move(self, from_, to):
return self.overlays.move(from_, to)
[docs] def remove(self, item):
self.overlays.remove(item)
self.__initProps.pop(item, None)
[docs] def clear(self):
del self[:]
class ProxyImage(fslimage.Image):
"""The ``ProxyImage`` class is a simple wrapper around an :class:`Image`
instance. It is intended to be used to represent images or data which
are derived from another image.
"""
def __init__(self, base, *args, **kwargs):
"""Create a ``ProxyImage``.
:arg base: The :class:`Image` instance upon which this ``ProxyImage``
is based.
:arg volume: Must be passed as a keyword argument. If provided, is a
slice into the image data specifying the 3D volume to
copy. If not provided, the entire image is copied.
"""
if not isinstance(base, fslimage.Image):
raise ValueError('Base image must be an Image instance')
self.__base = base
kwargs['header'] = base.header
volume = kwargs.pop('volume', None)
if volume is not None: data = base[volume]
else: data = base[:]
fslimage.Image.__init__(self, data, *args, **kwargs)
def getBase(self):
"""Returns the base :class:`Image` of this ``ProxyImage``. """
return self.__base
[docs]def findFEATImage(overlayList, overlay):
"""Searches the given :class:`.OverlayList` to see if there is a
:class:`.FEATImage` associated with the given ``overlay``. Returns the
``FEATImage`` if found, otherwise returns ``None``.
"""
import fsl.data.featanalysis as featanalysis
import fsl.data.featimage as featimage
if isinstance(overlay, featimage.FEATImage): return overlay
if overlay is None: return None
if overlay.dataSource is None: return None
featPath = featanalysis.getAnalysisDir(overlay.dataSource)
if featPath is None:
return None
dataPath = featanalysis.getDataFile(featPath)
featImage = overlayList.find(dataPath)
return featImage
[docs]def findMeshReferenceImage(overlayList, overlay):
"""Searches the :class:`.OverlayList` and tries to identify a reference
image for the given :class:`.Mesh` overlay. Returns the identified
overlay, or ``None`` if one can't be found.
"""
import fsl.data.vtk as fslvtk
# TODO support for gifti/freesurfer
if not isinstance(overlay, fslvtk.VTKMesh):
return None
try:
prefix = fslvtk.getFIRSTPrefix(overlay.dataSource)
for ovl in overlayList:
if prefix.startswith(ovl.name):
return ovl
except Exception:
pass
return None