Source code for fsleyes

#
# __init__.py - FSLeyes - a python based OpenGL image viewer.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""*FSLeyes* - a 3D image viewer.

This package contains the application logic for *FSLeyes*.


.. image:: images/fsleyes.png
   :scale: 50%
   :align: center


--------
Overview
--------


*FSLeyes* is an OpenGL application for displaying 3D :mod:`overlays
<.overlay>`. All overlays are stored in a single list, the
:class:`.OverlayList`. Only one ``OverlayList`` ever exists - this list is
shared throughout the application.  The primary overlay type is the NIFTI
image format; and a range of other formats are also supported including MGH
volumes, GIFTI and Freesurfer surface files, and VTK triangle meshes.


Amongst other things, *FSLeyes* provides the following features:

  - Orthographic view  (:mod:`.orthopanel`)
  - Lightbox view (:mod:`.lightboxpanel`)
  - 3D view (:mod:`.scene3dpanel`)
  - Time series plotting (:mod:`.timeseriespanel`)
  - Histogram plotting (:mod:`.histogrampanel`)
  - Power spectrum plotting (:mod:`.powerspectrumpanel`)
  - Jupyter notebook integration (:mod:`.notebook`)
  - FSL atlas explorer (:mod:`.atlaspanel`)
  - FEAT cluster results explorer (:mod:`.clusterpanel`)
  - Melodic component classification (:mod:`.melodicclassificationpanel`)
  - NIFTI image editing (:mod:`.editor`)
  - A comprehensive command line interface (:mod:`.parseargs`)


*FSLeyes* makes heavy use of the :mod:`fsleyes_props` project, which is an
event-based programming framework.


------------
Entry points
------------


*FSLeyes* may be started with the :func:`fsleyes.main.main` function. *FSLeyes*
also includes an off-screen screenshot generator called `render`, which may
be started via the :func:`fsleyes.render.main` function.


---------------------------
Frames, views, and controls
---------------------------


The :class:`.FSLeyesFrame` is the top level GUI object. It is a container for
one or more *views*. All views are defined in the :mod:`.views` sub-package,
and are sub-classes of the :class:`.ViewPanel` class. Currently there are two
primary view categories - :class:`.CanvasPanel` views, which use :mod:`OpenGL`
to display overlays, and :class:`.PlotPanel` views, which use
:mod:`matplotlib` to plot data related to the overlays.


View panels may contain one or more *control* panels which provide an
interface allowing the user to control some aspect of the view (e.g. the
:class:`.OverlayDisplayToolBar`), or to display some other data associated
with the overlays (e.g. the :class:`.ClusterPanel`).  All controls are
sub-classes of the :class:`.ControlPanel` or :class:`.ControlToolBar` classes,
and all built-in controls are defined in the :mod:`.controls` sub-package.


The view/control panel class hierarchy is shown below:

.. graphviz::

   digraph hierarchy {

     graph [size=""];

     node [style="filled",
           fillcolor="#ffffdd",
           fontname="sans"];

     rankdir="BT";
     1  [label="panel.FSLeyesPanel"];
     2  [label="views.viewpanel.ViewPanel"];
     3  [label="controls.controlpanel.ControlPanel"];
     4  [label="views.plotpanel.PlotPanel"];
     5  [label="views.canvaspanel.CanvasPanel"];
     6  [label="views.histogrampanel.HistogramPanel"];
     7  [label="<other plot panels>"];
     8  [label="views.orthopanel.OrthoPanel"];
     9  [label="<other canvas panels>"];
     10 [label="controls.overlaylistpanel.OverlayListPanel"];
     11 [label="<other control panels>"];

     2  -> 1;
     3  -> 1;
     4  -> 2;
     5  -> 2;
     6  -> 4;
     7  -> 4;
     8  -> 5;
     9  -> 5;
     10 -> 3;
     11 -> 3;
   }

All toolbars inherit from the :class:`.FSLeyesToolBar` base class:

.. graphviz::

   digraph toolbar_hierarchy {

     graph [size=""];

     node [style="filled",
           fillcolor="#ffffdd",
           fontname="sans"];

     rankdir="BT";
     1 [label="toolbar.FSLeyesToolBar"];
     2 [label="controls.controlpanel.ControlToolBar"];
     3 [label="controls.overlaydisplaytoolbar.OverlayDisplayToolBar"];
     4 [label="controls.lightboxtoolbar.LightBoxToolBar"];
     5 [label="<other toolbars>"];

     2 -> 1;
     3 -> 2;
     4 -> 2;
     5 -> 2;
   }


----------------------
The ``DisplayContext``
----------------------


In order to manage how overlays are displayed, *FSLeyes* uses a
:class:`.DisplayContext`. Because *FSLeyes* allows multiple views to be opened
simultaneously, it needs to use multiple ``DisplayContext`` instances.
Therefore, one master ``DisplayContext`` instance is owned by the
:class:`FSLeyesFrame`, and a child ``DisplayContext`` is created for every
:class:`.ViewPanel`. The display settings managed by each child
``DisplayContext`` instance can be linked to those of the master instance;
this allows display properties to be synchronised across displays.


Each ``DisplayContext`` manages a collection of :class:`.Display` objects, one
for each overlay in the ``OverlayList``. Each of these ``Display`` objects
manages a single :class:`.DisplayOpts` instance, which contains overlay
type-specific display properties. Just as child ``DisplayContext`` instances
can be synchronised with the master ``DisplayContext``, child ``Display`` and
``DisplayOpts`` instances can be synchronised to the master instances.


The above description is summarised in the following diagram:


.. image:: images/fsleyes_architecture.png
   :scale: 40%
   :align: center


In this example, two view panels are open - an :class:`.OrthoPanel`, and a
:class:`.LightBoxPanel`. The ``DisplayContext`` for each of these views, along
with their ``Display`` and ``DisplayOpts`` instances (one of each for every
overlay in the ``OverlayList``) are linked to the master ``DisplayContext``
(and its ``Display`` and ``DisplayOpts`` instances), which is managed by the
``FSLeyesFrame``.  All of this synchronisation functionality is provided by
the ``props`` package.


See the :mod:`~fsleyes.displaycontext` package documentation for more
details.


-----------------------
Events and notification
-----------------------

TODO


.. note:: The current version of FSLeyes (|version|) lives in the
          :mod:`fsleyes.version` module.
"""


import            os
import os.path as op
import            logging
import            warnings

from   fsl.utils.platform import platform as fslplatform
import fsl.utils.settings                 as fslsettings
import fsleyes.version                    as version


# The logger is assigned in
# the configLogging function
log = None


# If set to True, logging will not be configured
disableLogging = fslplatform.frozen


__version__ = version.__version__
"""The current *FSLeyes* version (read from the :mod:`fsleyes.version`
module).
"""


assetDir = op.join(op.dirname(__file__), '..')
"""Base directory which contains all *FSLeyes* assets/resources (e.g. icon
files). This is set in the :func:`initialise` function.
"""


[docs]def canWriteToAssetDir(): """Returns ``True`` if the user can write to the FSLeyes asset directory, ``False`` otherwise. """ return os.access(op.join(assetDir, 'assets'), os.W_OK | os.X_OK)
[docs]def initialise(): """Called when `FSLeyes`` is started as a standalone application. This function *must* be called before most other things in *FSLeyes* are used. Does a few initialisation steps:: - Initialises the :mod:`fsl.utils.settings` module, for persistent storage of application settings. - Sets the :data:`assetDir` attribute. """ global assetDir import matplotlib as mpl import fsleyes.plugins as plugins # implement various hacks and workarounds _hacksAndWorkarounds() # Initialise the fsl.utils.settings module fslsettings.initialise('fsleyes') # initialise FSLeyes plugins (will discover # any plugins saved in the settings dir) plugins.initialise() # Tell matplotlib what backend to use. # n.b. this must be called before # matplotlib.pyplot is imported. mpl.use('WxAgg') # The fsleyes.actions.frameactions module # monkey-patches some things into the # FSLeyesFrame class, so it must be # imported immediately after fsleyes.frame. import fsleyes.frame # noqa import fsleyes.actions.frameactions # noqa fsleyesDir = op.dirname(__file__) assetDir = None options = [] # If we are running from a bundled # application, we'll guess at the # location, which will differ depending # on the platform if fslplatform.frozen: mac = op.join(fsleyesDir, '..', '..', '..', '..', 'Resources') lnx = op.join(fsleyesDir, '..', 'share', 'FSLeyes') options.append(op.normpath(mac)) options.append(op.normpath(lnx)) # Otherwise we are running from a code install, # or from a source distribution. The assets # directory is either inside, or alongside, the # FSLeyes package directory. else: options = [op.join(fsleyesDir, '..'), fsleyesDir] for opt in options: if op.exists(op.join(opt, 'assets')): assetDir = op.abspath(opt) break if assetDir is None: raise RuntimeError('Could not find FSLeyes asset directory! ' 'Searched: {}'.format(options))
[docs]def _hacksAndWorkarounds(): """Called by :func:`initialise`. Implements hacks and workarounds for various things. """ # Under wxPython/Phoenix, the # wx.html package must be imported # before a wx.App has been created import wx.html # noqa # PyInstaller 3.2.1 forces matplotlib to use a # temporary directory for its settings and font # cache, and then deletes the directory on exit. # This is silly, because the font cache can take # a long time to create. Clearing the environment # variable should cause matplotlib to use # $HOME/.matplotlib (or, failing that, a temporary # directory). # # https://matplotlib.org/faq/environment_variables_faq.html#\ # envvar-MPLCONFIGDIR # # https://github.com/pyinstaller/pyinstaller/blob/v3.2.1/\ # PyInstaller/loader/rthooks/pyi_rth_mplconfig.py # # n.b. This will cause issues if building FSLeyes # with the pyinstaller '--onefile' option, as # discussed in the above pyinstaller file. if fslplatform.frozen: os.environ.pop('MPLCONFIGDIR', None) # nibabel rejects NIfTI images where the # quaternion vector has a length greater # than 1. This is fine, as it is mandated # by the NIfTI spec. But FSL is much more # lenient than nibabel, and nibabel can # also reject some qforms due to float32 # imprecision. So here we're increasing # the tolerance of nibabel to strange # qforms. import nibabel as nib nib.Nifti1Header.quaternion_threshold = -1e5 # OSX sometimes sets the local environment # variables to non-standard values, which # breaks the python locale module. # # http://bugs.python.org/issue18378 try: import locale locale.getdefaultlocale() except ValueError: os.environ['LC_ALL'] = 'C.UTF-8'
[docs]def configLogging(verbose=0, noisy=None): """Configures *FSLeyes* ``logging``. .. note:: All logging calls are usually stripped from frozen versions of *FSLeyes*, so this function does nothing when we are running a frozen version. :arg verbose: A number between 0 and 3, indicating the verbosity level. :arg noisy: A sequence of module names - logging will be enabled on these modules. """ global log # already configured if log is not None: return if noisy is None: noisy = [] # Show deprecations if running from code if fslplatform.frozen: warnings.filterwarnings('ignore', category=DeprecationWarning) else: warnings.filterwarnings('default', category=DeprecationWarning) # Set up the root logger logFormatter = logging.Formatter('%(levelname)8.8s ' '%(filename)20.20s ' '%(lineno)4d: ' '%(funcName)-15.15s - ' '%(message)s') logHandler = logging.StreamHandler() logHandler.setFormatter(logFormatter) log = logging.getLogger() log.addHandler(logHandler) # Everything below this point sets up verbosity # as requested by the user. But verbosity-related # command line arguments are not exposed to the # user in frozen versions of FSLeyes, so if we're # running as a frozen app, there's nothing else # to do. if disableLogging: return # Now we can set up logging if verbose == 1: log.setLevel(logging.DEBUG) # make some noisy things quiet logging.getLogger('fsleyes.gl') .setLevel(logging.WARNING) logging.getLogger('fsleyes.views') .setLevel(logging.WARNING) logging.getLogger('fsleyes_props') .setLevel(logging.WARNING) logging.getLogger('fsleyes_widgets').setLevel(logging.WARNING) elif verbose == 2: log.setLevel(logging.DEBUG) logging.getLogger('fsleyes_props') .setLevel(logging.WARNING) logging.getLogger('fsleyes_widgets').setLevel(logging.WARNING) elif verbose == 3: log.setLevel(logging.DEBUG) logging.getLogger('fsleyes_props') .setLevel(logging.DEBUG) logging.getLogger('fsleyes_widgets').setLevel(logging.DEBUG) for mod in noisy: logging.getLogger(mod).setLevel(logging.DEBUG) # The trace module monkey-patches some # things if its logging level has been # set to DEBUG, so we import it now so # it can set itself up. traceLogger = logging.getLogger('fsleyes_props.trace') if traceLogger.getEffectiveLevel() <= logging.DEBUG: import fsleyes_props.trace # noqa