Source code for anatomist.cpp

# -*- coding: utf-8 -*-
#  This software and supporting documentation are distributed by
#      Institut Federatif de Recherche 49
#      CEA/NeuroSpin, Batiment 145,
#      91191 Gif-sur-Yvette cedex
#      France
#
# This software is governed by the CeCILL license version 2 under
# French law and abiding by the rules of distribution of free software.
# You can  use, modify and/or redistribute the software under the
# terms of the CeCILL license version 2 as circulated by CEA, CNRS
# and INRIA at the following URL "http://www.cecill.info".
#
# As a counterpart to the access to the source code and  rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty  and the software's author,  the holder of the
# economic rights,  and the successive licensors  have only  limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading,  using,  modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean  that it is complicated to manipulate,  and  that  also
# therefore means  that it is reserved for developers  and  experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and,  more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license version 2 and that you accept its terms.


'''
Low level module containing Sip bindings of Anatomist C++ API.

Introduction
============

This module is mostly bindings to the C++ library of Anatomist. A few classes
have been slightly modified or rewritten, either to hide garbage or to
appear more pythonic.

For typical use, this module will not be called directly but throught general API. But it can be needed for using advanced features.

The main entry point is the :class:`Anatomist` class which must be instantiated before any operation can be performed.

>>> import anatomist.cpp as anatomist
>>> a = anatomist.Anatomist()

Note that instantiating Anatomist also instantiates the
`Qt <http://www.qt.io>`_ QApplication, but does not run the Qt event loop,
so python interactive shells are still accessible after that operation, but
the GUI is frozen until the event loop is executed, using the following PyQt
code:

>>> # from PyQt4 import Qt
>>> # or, to switch to the correct Qt implementation and bindings (Qt4/Qt5)
>>> from soma.qt_gui.qt_backend import Qt
>>> Qt.qApp.exec_loop()

but then the loop does not return until the GUI application is over, so you
should start it at the end of your code.

Another comfortable alternative to the GUI loop problem in interactive
python shells is to use `IPython <http://ipython.scipy.org/>`_ with the
option ``--gui=qt``: IPython is an interactive python shell with many
improvements, and which can run in a different thread than the GUI, so that
the GUI can run without hanging the shell.

Contrarily to the Qt application, Anatomist can be instantiated multiple
times (it is not a singleton, but contains a singleton).

In addition to providing bindings to the Anatomist C++ API, the anatomist
module also loads some python modules in anatomist: every python module
found in the following locations are loaded::

    os.path.join(str(anatomist.Anatomist().anatomistSharedPath()), 'python_plugins')
    os.path.join(str(anatomist.Anatomist().anatomistHomePath()), 'python_plugins')

Main concepts
=============
There are many pieces in Anatomist library, but a few are really needed to
begin with.

* the Anatomist application, the L{Anatomist} class instance, is responsible
  for many global variables, the application state, and the startup
  procedure (including plugins loading). The application is used internally
  in many functions.
* objects: :class:`AObject` class and subclasses
* windows: :class:`AWindow` class and subclasses
* the commands processor: a singleton :class:`Processor` instance obtained via
  the application: :meth:`Anatomist.theProcessor`. The processor is a commands
  interpreter for the rudimentary language of Anatomist. It is also
  responsible of saving every command executed in the history file
  ``$HOME/.anatomist/history.ana`` so most of the session can be replayed.
  Many operations are done via commands and the processor: creating windows,
  loading objects, displaying them, etc. so this element is probably the
  most important part of Anatomist.
* conversions between AIMS object and Anatomist objects:
  :class:`AObjectConverter` is here to perform this task.
'''

from __future__ import print_function
from __future__ import absolute_import

import os
import sys
import string
import glob
import operator
import types
import numpy
import six
import collections
import logging


def isSequenceType(item):
    if isinstance(item, collections.Sequence):
        return True
    if hasattr(item, 'isArray'):
        # aims Object API
        return item.isArray()
    if isMappingType(item):
        return False
    methods = ['__getitem__', '__contains__', '__iter__', '__len__']
    # should also include: count, index but pyaims sequences do not have them
    for m in methods:
        if not hasattr(item, m):
            return False
    return True


def isMappingType(item):
    if isinstance(item, collections.Mapping):
        return True
    if hasattr(item, 'isDictionary'):
        # aims Object API
        return item.isDictionary()
    methods = ['get', 'items', 'keys', 'values', '__getitem__', '__iter__',
               '__contains__', '__len__']
    if six.PY2:
        methods.append('iteritems')
    for m in methods:
        if not hasattr(item, m):
            return False
    return True


path = os.path.dirname(__file__)
if path not in sys.path:
    sys.path.insert(0, path)

# add path for python modules lib
if sys.platform[:3] == 'win':
    sep = ';'
else:
    sep = ':'

# PYTHONPATH. On python 2.3, it doesn't seem to be taken into account
# when pyhton is run from a library
path = os.getenv('PYTHONPATH')
if path is not None:
    for x in path.split(sep):
        if x not in sys.path:
            sys.path.append(x)
    del x
del path, sep

from soma import aims
from soma.importer import ExtendedImporter

# force using sip API v2 for PyQt4
sip_classes = ['QString', 'QVariant', 'QDate', 'QDateTime',
               'QTextStream', 'QTime', 'QUrl']
_sip_api_set = False
import sip
for sip_class in sip_classes:
    try:
        sip.setapi(sip_class, 2)
    except ValueError as e:
        if not _sip_api_set:
            logging.warning(e.message)
    _sip_api_set = True

# cleanup namespaces in Sip-generated code
ExtendedImporter().importInModule('', globals(), locals(), 'anatomistsip')
ExtendedImporter().importInModule('', globals(), locals(), 'anatomistsip',
                                  ['anatomistsip.anatomist'])
del ExtendedImporter

from soma.qt_gui import qt_backend
qt_backend.set_qt_backend(compatible_qt5=True, pyqt_api=2)

from anatomistsip import *

aims.__fixsipclasses__(list(globals().items()))

loaded_modules = []
global _anatomist_modsloaded
_anatomist_modsloaded = 0

# update AObjectConverter class to be more flexible


def aimsFromAnatomist(ao, options={'scale': 1}):
    tn = ao.objectTypeName(ao.type())
    # take into account wrapping case
    if hasattr(ao, 'internalRep'):
        ao = ao.internalRep
    if tn == 'VOLUME':
        try:
            hdr = ao.attributed()
            if hdr:
                dt = hdr['data_type']
                oc = getattr(AObjectConverter, 'aimsData_' + dt)
                aim = oc(ao, options)
                if not aim.isNull():
                    return aim._get()
        except:
            pass
        # all this just in case data_type is not set in header
        aim = AObjectConverter.aimsData_U8(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_S16(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_U16(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_S32(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_U32(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_FLOAT(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_DOUBLE(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_RGB(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsData_RGBA(ao, options)
        if not aim.isNull():
            return aim._get()
    elif tn == 'SURFACE':
        aim = AObjectConverter.aimsSurface3(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsSurface4(ao, options)
        if not aim.isNull():
            return aim._get()
        aim = AObjectConverter.aimsSurface2(ao, options)
        if not aim.isNull():
            return aim._get()
    elif tn == 'BUCKET':
        aim = AObjectConverter.aimsBucketMap_VOID(ao, options)
        if not aim.isNull():
            return aim._get()
    elif tn == 'TEXTURE':
        at = ao.attributed()
        if not at:
            dt = 'FLOAT'
        else:
            try:
                dt = at['data_type']
            except:
                dt = 'FLOAT'
        try:
            conv = getattr(AObjectConverter, 'aimsTexture_' + dt)
            aim = conv(ao, options)._get()
            return aim
        except:
            aim = AObjectConverter.aimsTexture_FLOAT(ao, options)
            if not aim.isNull():
                return aim._get()
            aim = AObjectConverter.aimsTexture_POINT2DF(ao, options)
            if not aim.isNull():
                return aim._get()
            aim = AObjectConverter.aimsTexture_S16(ao, options)
            if not aim.isNull():
                return aim._get()
            aim = AObjectConverter.aimsTexture_S32(ao, options)
            if not aim.isNull():
                return aim._get()
            aim = AObjectConverter.aimsTexture_U32(ao, options)
        if aim and not aim.isNull():
            return aim._get()
        return None
    elif tn == 'GRAPH':
        aim = AObjectConverter.aimsGraph(ao, options)
        if not aim.isNull():
            return aim._get()
    elif tn == 'NOMENCLATURE':
        aim = AObjectConverter.aimsTree(ao, options)
        if not aim.isNull():
            return aim._get()
    elif tn == 'SparseMatrix':
        aim = AObjectConverter.aimsSparseMatrix(ao, options)
        if not aim.isNull():
            return aim._get()
    return None


AObjectConverter.aims = staticmethod(aimsFromAnatomist)
del aimsFromAnatomist


def anatomistFromAims(obj):
    ot = type(obj).__name__
    if isinstance(obj, AObject):
        return obj
    t = None
    if isinstance(obj, numpy.ndarray):
        t = None
        if obj.dtype is numpy.dtype(numpy.int8):
            t = aims.Volume_S8
        elif obj.dtype is numpy.dtype(numpy.uint8):
            t = aims.Volume_U8
        elif obj.dtype is numpy.dtype(numpy.int16):
            t = aims.Volume_S16
        elif obj.dtype is numpy.dtype(numpy.uint16):
            t = aims.Volume_U16
        elif obj.dtype is numpy.dtype(numpy.int_) \
                or obj.dtype is numpy.dtype(numpy.int32):
            t = aims.Volume_S32
        elif obj.dtype is numpy.dtype(numpy.uint32):
            t = aims.Volume_U32
        elif obj.dtype is numpy.dtype(numpy.float32):
            t = aims.Volume_FLOAT
        elif obj.dtype is numpy.dtype(numpy.float64) \
                or obj.dtype is numpy.dtype(numpy.float_):
            t = aims.Volume_DOUBLE
    if t:
        return AObjectConverter.anatomist(t(obj))
    return AObjectConverter._anatomist(obj)


AObjectConverter._anatomist = AObjectConverter.anatomist
AObjectConverter.anatomist = staticmethod(anatomistFromAims)
del anatomistFromAims


# Anatomist class: entry point to anatomist

[docs]class Anatomist(AnatomistSip): ''' Anatomist class: entry point to anatomist ''' def __init__(self, *args): import anatomistsip import os import sys import glob import traceback global _anatomist_modsloaded modsloaded = _anatomist_modsloaded _anatomist_modsloaded = 1 AnatomistSip.__init__(self, ('anatomist', ) + args) if modsloaded: return pythonmodules = os.path.join(str(self.anatomistSharedPath()), 'python_plugins') homemodules = os.path.join(str(self.anatomistHomePath()), 'python_plugins') print('global modules:', pythonmodules) print('home modules:', homemodules) if sys.path[0] != homemodules: sys.path.insert(0, homemodules) if sys.path[1] != pythonmodules: sys.path.insert(1, pythonmodules) mods = glob.glob(os.path.join(pythonmodules, '*')) \ + glob.glob(os.path.join(homemodules, '*')) # print('modules:', mods) global loaded_modules for x in mods: if os.path.basename(x).startswith('_'): # don't load files starting with '_' (__init__, __pycache__...) continue if x[-4:] == '.pyo': x = x[:-4] elif x[-4:] == '.pyc': x = x[:-4] elif x[-3:] == '.py': x = x[:-3] elif not os.path.isdir(x): continue # print('module:', x) x = os.path.basename(x) if x in loaded_modules: continue loaded_modules.append(x) print('loading module', x) try: six.exec_('import ' + x, {}, {}) except: print() print('loading of module', x, 'failed:') exceptionInfo = sys.exc_info() e, v, t = exceptionInfo tb = traceback.extract_tb(t) try: name = e.__name__ except: name = str(e) print(name, ':', v) print('traceback:') for file, line, function, text in tb: if text is None: text = '?' print(file, '(', line, ') in', function, ':') print(text) print() # must explicitely delete reference to frame objects # (traceback) else it creates a reference cycle and the object # cannot be deleted del e, v, t, tb, exceptionInfo print('all python modules loaded')
# Processor.execute def newexecute(self, *args, **kwargs): ''' Commands execution. It can be used in several different forms: * execute(Command) * execute(commandname, params=None, context=None) * execute(commandname, context=None, **kwargs) Parameters ---------- Command: :class:`Command` subclass command to be executed commandname: str name of the command to executed params: dict or str optional parameters (default: None) context: :class:`CommandContext` command execution context (default: None) kwargs: keyword arguments which are passed to Anatomist directly in the command Returns ------- command: the executed command (or None) ''' def replace_dict(dic, cc): for k, v in dic.items(): if isinstance(v, AObject) or isinstance(v, AWindow) \ or isinstance(v, Referential) or isinstance(v, Transformation): try: i = cc.id(v) except: i = cc.makeID(v) dic[k] = i elif hasattr(v, 'items'): # operator.isMappingType( v ): replace_dict(v, cc) elif not isinstance(v, six.string_types) and isSequenceType(v): replace_list(v, cc) def replace_list(l, cc): k = 0 for v in l: if isinstance(v, AObject) or isinstance(v, AWindow)\ or isinstance(v, Referential) or isinstance(v, Transformation): try: i = cc.id(v) except: i = cc.makeID(v) l[k] = i elif hasattr(v, 'items'): # operator.isMappingType( v ): replace_dict(v, cc) elif not isinstance(v, six.string_types) and isSequenceType(v): replace_list(v, cc) k += 1 if len(args) < 1 or len(args) > 3: raise RunTimeError('wrong arguments number') if type(args[0]) is not str: if len(args) != 1: raise RunTimeError('wrong arguments number') return self._execute(args[0]) dic = {} cc = None if len(kwargs) != 0: if len(args) > 2: raise RunTimeError('wrong arguments number') kw = 0 if len(args) == 2: cc = args[1] else: cc = kwargs.get('context') if cc is not None: kw = 1 dic = kwargs.copy() if kw: del dic['context'] elif len(args) >= 2: dic = args[1] if len(args) == 3: cc = args[2] elif len(args) > 3: raise RunTimeError('wrong arguments number') if not cc: cc = CommandContext.defaultContext() replace_dict(dic, cc) return self._execute(args[0], str(dic), cc) Processor.execute = newexecute del newexecute # automatically wrap creator functions in controls system class PyKeyActionLink(Control.KeyActionLink): def __init__(self, method): Control.KeyActionLink.__init__(self) self._method = method def execute(self): self._method() def clone(self): return PyKeyActionLink(self._method) class PyMouseActionLink(Control.MouseActionLink): def __init__(self, method): Control.MouseActionLink.__init__(self) self._method = method def execute(self, x, y, globalX, globalY): self._method(x, y, globalX, globalY) def clone(self): return PyMouseActionLink(self._method) def action(self): return self._method.__self__ class PyWheelActionLink(Control.WheelActionLink): def __init__(self, method): Control.WheelActionLink.__init__(self) self._method = method def execute(self, delta, x, y, globalX, globalY): self._method(delta, x, y, globalX, globalY) def clone(self): return PyWheelActionLink(self._method) class PySelectionChangedActionLink(Control.SelectionChangedActionLink): def __init__(self, method): Control.SelectionChangedActionLink.__init__(self) self._method = method def execute(self): self._method() def clone(self): return PySelectionChangedActionLink(self._method) class PyActionCreator(ActionDictionary.ActionCreatorBase): def __init__(self, function): ActionDictionary.ActionCreatorBase.__init__(self) self._function = function def __call__(self): return self._function() class PyControlCreator(ControlDictionary.ControlCreatorBase): def __init__(self, function): ControlDictionary.ControlCreatorBase.__init__(self) self._function = function def __call__(self): return self._function() ControlDictionary.addControl = \ lambda self, name, creator, prio, allowreplace=False: \ self._addControl(name, PyControlCreator(creator), prio, allowreplace) ActionDictionary.addAction = \ lambda self, name, creator: \ self._addAction(name, PyActionCreator(creator)) # control subscribe functions def keyPressSubscribeFunction(self, key, state, func, name=''): if name == '': if hasattr(func, '__name__'): name = func.__name__ elif hasattr(func, '__func__') and hasattr(func.__func__, '__name__'): name = func.__func__.__name__ self._keyPressEventSubscribe(key, state, PyKeyActionLink(func), name) def keyReleaseSubscribeFunction(self, key, state, func, name=''): if name == '': if hasattr(func, '__name__'): name = func.__name__ elif hasattr(func, '__func__') and hasattr(func.__func__, '__name__'): name = func.__func__.__name__ self._keyReleaseEventSubscribe(key, state, PyKeyActionLink(func), name) def mousePressSubscribeFunction(self, but, state, func, name=''): if name == '': if hasattr(func, '__name__'): name = func.__name__ elif hasattr(func, '__func__') and hasattr(func.__func__, '__name__'): name = func.__func__.__name__ self._mousePressButtonEventSubscribe( but, state, PyMouseActionLink(func), name) def mouseReleaseSubscribeFunction(self, but, state, func, name=''): if name == '': if hasattr(func, '__name__'): name = func.__name__ elif hasattr(func, '__func__') and hasattr(func.__func__, '__name__'): name = func.__func__.__name__ self._mouseReleaseButtonEventSubscribe( but, state, PyMouseActionLink(func), name) def mouseDoubleClickSubscribeFunction(self, but, state, func, name=''): if name == '': if hasattr(func, '__name__'): name = func.__name__ elif hasattr(func, '__func__') and hasattr(func.__func__, '__name__'): name = func.__func__.__name__ self._mouseDoubleClickEventSubscribe( but, state, PyMouseActionLink(func), name) def mouseMoveSubscribeFunction(self, but, state, func, name=''): if name == '': if hasattr(func, '__name__'): name = func.__name__ elif hasattr(func, '__func__') and hasattr(func.__func__, '__name__'): name = func.__func__.__name__ self._mouseMoveEventSubscribe(but, state, PyMouseActionLink(func), name) Control.keyPressEventSubscribe = keyPressSubscribeFunction Control.keyReleaseEventSubscribe = keyReleaseSubscribeFunction Control.mousePressButtonEventSubscribe = mousePressSubscribeFunction Control.mouseReleaseButtonEventSubscribe = mouseReleaseSubscribeFunction Control.mouseMoveEventSubscribe = mouseMoveSubscribeFunction Control.mouseDoubleClickEventSubscribe = mouseDoubleClickSubscribeFunction del keyPressSubscribeFunction, keyReleaseSubscribeFunction, \ mousePressSubscribeFunction, mouseReleaseSubscribeFunction, \ mouseDoubleClickSubscribeFunction, mouseMoveSubscribeFunction Control.mouseLongEventSubscribe = lambda self, but, state, startfunc, \ longfunc, endfunc, exclusive: \ self._mouseLongEventSubscribe( but, state, PyMouseActionLink(startfunc), PyMouseActionLink(longfunc), PyMouseActionLink(endfunc), exclusive) Control.wheelEventSubscribe = lambda self, func: \ self._wheelEventSubscribe( PyWheelActionLink(func)) Control.selectionChangedEventSubscribe = lambda self, func: \ self._selectionChangedEventSubscribe( PySelectionChangedActionLink(func)) aims.convertersObjectToPython.update({ 'AObject': AObject.fromObject, 'PN9anatomist7AObjectE': AObject.fromObject, 'N5carto10shared_ptrIN9anatomist7AObjectEEE': AObject.fromObject, 'PN9anatomist7AWindowE': AWindow.fromObject, }) # Import external python modules from . import mobject # delete things from other modules # apply changes to config properties to Anatomist internal state def __GlobalConfiguration_setitem__(self, param, value): print('config.setitem:', param, ':', value) super(GlobalConfiguration, self).__setitem__(param, value) self.apply() anatomist.GlobalConfiguration.__setitem__ = __GlobalConfiguration_setitem__ del __GlobalConfiguration_setitem__ del os, string, glob del anatomist # , aims # ------------- # docs Command.__doc__ = ''' Commands are the execution units of the :class:`Processor`. Commands are used as subclasses of :class:`Command`. They can be built either on the fly by the programmer, or using the commands factory via the :meth:`Processor.execute` function. Never call :meth:`Command.execute` or :meth:`Command.doit` directly: only the processor will do so. Once built, a command must always be executed by the processor: >>> a = anatomist.Anatomist() >>> c = anatomist.CreateWindowCommand('Axial') >>> a.theProcessor().execute(c) But in any case there is a more handy shortcut in the higher-level API: :meth:`anatomist.base.Anatomist.execute` Comamnds can be subclassed in Python. To be valid, a new command must define at least the :meth:`name` and :meth:`doit` methods. :meth:`doit` will actually do the execution and your program may make it do anything. Later, :meth:`read` and :meth:`write` should also be redefined to allow proper IO for this command (when the commands IO and factory are exported from C++ to python). :meth:`Command.doit` receives no parameters (apart from ``self``). All execution arguments must be set in the command itself upon construction (either by the constructor or by setting some instance variables). One important parameter which a command would often use is the :class:`CommandContext`, which specifies some IO streams for output printing and communication with external programs, and an identifiers set used to name and identify objects (in a general meaning: including windows etc.) through this IO streams. ''' AObjectConverter.__doc__ = ''' Aims / Anatomist objects converter Only two static methods are useful in this class: * :meth:`AObjectConverter.anatomist` converts an AIMS (or possibly other) object into an Anatomist object (:class:`AObject` subclass), if possible * :meth:`AObjectConverter.aims` converts an Anatomist object to an AIMS object, if possible Conversions are generally done by wrapping or embedding, or by extracting a reference to an internal object, so objects data are genrally shared between the AIMS and the Anatomist objects layers. This allows internal modification of Anatomist objects data. After a modification through the Aims (lower level) layer API, modification notification must be issued to the Anatomist layer to update the display of the object in Anatomist. This is generally done via the :meth:`AObject.setChanged` and :meth:`AObject.notifyObservers` methods. ''' private = ['private', 'anatomistsip', 'AnatomistSip', 'numpy', 'operator', 'mobject'] __all__ = [] for x in dir(): if not x.startswith('_') and x not in private: __all__.append(x) del x, private