diff --git a/MANIFEST.in b/MANIFEST.in index 47cfa75..00c493e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,5 @@ -include *.txt *.py -include pylocator/camera.png pylocator/image_reader.glade -recursive-include pylocator *.rst *.py -graft doc -graft doc/_static -graft doc/_templates +include README LICENSE +recursive-include pylocator *.py +recursive-include doc * +exclude doc/build/* global-exclude *~ *.swp diff --git a/README b/README index ec246a8..975806f 100644 --- a/README +++ b/README @@ -1,38 +1,50 @@ -The PyLocator-program -===================== +PyLocator +========= -A little program for localization of EEG-electrodes from MRI-recordings. -Uses VTK for a neat 3d-interface. +PyLocator is a Qt-based visualisation tool for inspecting 3-D NIfTI volumes +and annotating EEG electrodes. The modern port replaces the historic PyGTK +interface with a PySide6/VTK application that runs on contemporary Linux and +Windows systems. -Building the docs ----------------------- +Installation +------------ -To build the docs you need to have setuptools and sphinx (>=0.5) installed. -Run the command:: +Create and activate a Python 3.11 virtual environment and install the runtime +dependencies with:: - python setup.py build_sphinx + pip install -r requirements.txt + +You can then install PyLocator itself in editable mode while keeping the CLI +entry point available:: -The docs are built in the build/sphinx/html directory. + pip install -e . +Running PyLocator +----------------- -Making a source tarball ----------------------------- +Once installed you can start the GUI directly from the command line:: -To create a source tarball, eg for packaging or distributing, run the -following command:: + pylocator - python setup.py sdist +When no filename is supplied the application starts with an empty viewport and +provides an **Open…** action to select a NIfTI file. The status bar and the +volume information dock show basic metadata such as voxel spacing and the value +range of the currently loaded dataset. -The tarball will be created in the `dist` directory. This command will -compile the docs, and the resulting tarball can be installed with -no extra dependencies than the Python standard library. You will need -setuptool and sphinx. +Documentation +------------- -Making a release and uploading it to PyPI ------------------------------------------- +The Sphinx documentation that ships with the legacy project is still available +and can be built with:: + + python setup.py build_sphinx -This command is only run by project manager, to make a release, and -upload in to PyPI:: +The generated HTML pages are written to ``doc/build``. - python setup.py sdist bdist_egg register upload +Development notes +----------------- +The new Qt port deliberately focuses on volume loading and rendering first. +Legacy features such as marker editing, surface visualisation and undo/redo +have not been migrated yet. Contributions that rebuild these workflows on top +of the modern architecture are very welcome. diff --git a/bin/pylocator b/bin/pylocator index fa8f0c7..972a7a6 100644 --- a/bin/pylocator +++ b/bin/pylocator @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/env python3 """Run pylocator""" import getopt @@ -17,8 +17,8 @@ def main(): def info_exit(option = None, value = None): """Print usage information and abort execution""" if not (option in ["-h", "--help"] or option == None): - print "Wrong argument:", option, ",", value - print USAGE + print("Wrong argument:", option, ",", value) + print(USAGE) sys.exit(0) try: options, args = getopt.getopt( diff --git a/doc/src/conf.py b/doc/src/conf.py index c567eba..2dd26a0 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -41,8 +41,8 @@ master_doc = 'index' # General information about the project. -project = u'PyLocator' -copyright = u'2011-2012, Thorsten Kranz' +project = 'PyLocator' +copyright = '2011-2012, Thorsten Kranz' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -168,8 +168,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ - ('index', 'pylocator.tex', ur'PyLocator Documentation', - ur'Thorsten Kranz', 'manual'), + ('index', 'pylocator.tex', r'PyLocator Documentation', + r'Thorsten Kranz', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/doc/src/install.rst b/doc/src/install.rst index f2bcf62..615726e 100644 --- a/doc/src/install.rst +++ b/doc/src/install.rst @@ -1,111 +1,59 @@ Installation ============ -.. index:: dependencies - Dependencies -------------- -PyLocator relies on a bunch of libraries: - -* `VTK `_: 3d-visualization -* `nibabel `_: Reading the Nifti-format for MRI data -* `NumPy `_: Sophisticated array-types for Python -* `GTK+ `_: For the GUI. -* `GTK+ OpenGL Extension `_ - -On a Debian-like environement, these dependencies can usually be resolved via a simple:: +------------ - sudo apt-get install python-vtk python-nibabel python-numpy python-gtk2 python-gtkglext1 +PyLocator now relies on a modern Qt/VTK stack: -This should prepare your system for PyLocator. On Windows and OS X, things are a little bit -more complicated, but Python distributions like `EPD `_ -or `Python(x,y) `_ should be helpful here. The main problem will be to -get **gtkgtlext** working. If you have any hints, e.g., binary packages, please let me know. +* `VTK `_ for 3-D visualisation +* `Nibabel `_ for reading NIfTI volumes +* `NumPy `_ for numerical data handling +* `PySide6 `_ for the Qt user interface -Depending on your configuration, nibabel has to be downloaded separately. +The recommended way to install these dependencies is via ``pip`` inside a +Python 3.11 virtual environment:: -Can you help with detailed instructions for these operating systems? Please tell me! + python -m venv .venv + source .venv/bin/activate + pip install -r requirements.txt +Running ``pip install -e .`` afterwards exposes the ``pylocator`` console entry +point and keeps the working copy editable. How to download --------------- -The source code of PyLocator is kept in a public GIT repository: - -http://www.github.com/nipy/PyLocator - -.. index:: repository -.. index:: source code - -You can simply clone this repository via:: - - git clone git://github.com/nipy/PyLocator.git - -There is a binary package for Debian-like systems (Debian, Ubuntu, ...) available. -It will make sure that all requirements are installed and add an entry to your -applications menu. - -.. image:: _static/download_deb.png - :align: center - :target: http://pylocator.thorstenkranz.de/download/pylocator_1.0_all.deb - -It is tested with Ubuntu Oneiric and Natty. If you successfully installed it on -other flavours and versions, please let me know. -Alternatively, you can download a tarball that is updated once in a while from +The source code lives on GitHub:: -http://pylocator.thorstenkranz.de/download/pylocator_1.0b3.tar.gz + git clone https://github.com/nipy/PyLocator.git -Extract this archive using your preferred archive manager or in a terminal using something like:: - - tar xfvz pylocator_1.0b3.tar.gz +You can use the repository directly for development or installation. How to install ---------------- -From Debian-package -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Installation of Debian packages should be straight-forward. Usually you just have -to double click the downloaded file and confirm the installation. If you prefer to -work in a terminal, you can:: - - sudo dpkg -i pylocator_1.0b3_all.deb - -That's it! +-------------- From PyPI -^^^^^^^^^^^^^^^^^^^^ -PyLocator is registered at PyPI, the Python Package Index. This makes -installation easy also for non-debian systems. -If you have setuptools installed, simply type:: +^^^^^^^^^ - sudo easy_install PyLocator +The PyPI package can be installed with:: -and the setuptools will do the magic for you. + pip install pylocator From source -^^^^^^^^^^^^^^^^^^^^ -Once you obtained the source code, enter the PyLocator-directory and type:: - - python setup.py build - python setup.py install --user # For per-user installation - # or - sudo python setup.py install # system-wide installation - -After these steps, the package *pylocator* should be properly installed. You can then run the program -by running:: +^^^^^^^^^^^ - python ~/.local/lib/python2.?/site-packages/pylocator/pylocator.py +If you are working from a clone simply run:: -in case of a per-user installation or:: + pip install -e . - python /usr/local/lib/python2.?/site-packages/pylocator/pylocator.py - -or similar in case of a system-wide installation. Replace the *2.?* by your python version number. - -This solution isn't perfect yet, I'll clean it up soon. Of course you can create some little bash-script -that calls this for you and put it into ~/bin/pylocator or similar:: - - #! /bin/bash - python ~/.local/lib/python2.?/site-packages/pylocator/pylocator.py $@ +Afterwards the ``pylocator`` command is available on your ``PATH``:: + pylocator /path/to/volume.nii.gz +Binary packages +--------------- +The historical Debian packages for the GTK version are no longer maintained. +Building native packages for the Qt port is on the roadmap; contributions and +packaging notes are welcome. diff --git a/doc/src/sphinxext/autosummary.py b/doc/src/sphinxext/autosummary.py index 6feddba..944aa5b 100644 --- a/doc/src/sphinxext/autosummary.py +++ b/doc/src/sphinxext/autosummary.py @@ -59,7 +59,7 @@ import sphinx.addnodes, sphinx.roles from sphinx.util import patfilter -from docscrape_sphinx import get_doc_object +from .docscrape_sphinx import get_doc_object def setup(app): @@ -288,7 +288,7 @@ def _import_by_name(name): # ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ... last_j = 0 modname = None - for j in reversed(range(1, len(name_parts)+1)): + for j in reversed(list(range(1, len(name_parts)+1))): last_j = j modname = '.'.join(name_parts[:j]) try: @@ -305,7 +305,7 @@ def _import_by_name(name): return obj else: return sys.modules[modname] - except (ValueError, ImportError, AttributeError, KeyError), e: + except (ValueError, ImportError, AttributeError, KeyError) as e: raise ImportError(e) #------------------------------------------------------------------------------ diff --git a/doc/src/sphinxext/autosummary_generate.py b/doc/src/sphinxext/autosummary_generate.py index 2c65097..73641e2 100755 --- a/doc/src/sphinxext/autosummary_generate.py +++ b/doc/src/sphinxext/autosummary_generate.py @@ -15,10 +15,10 @@ """ import glob, re, inspect, os, optparse, pydoc -from autosummary import import_by_name +from .autosummary import import_by_name try: - from phantom_import import import_phantom_module + from .phantom_import import import_phantom_module except ImportError: import_phantom_module = lambda x: x @@ -42,7 +42,7 @@ def main(): # read names = {} - for name, loc in get_documented(args).items(): + for name, loc in list(get_documented(args).items()): for (filename, sec_title, keyword, toctree) in loc: if toctree is not None: path = os.path.join(os.path.dirname(filename), toctree) @@ -58,8 +58,8 @@ def main(): try: obj, name = import_by_name(name) - except ImportError, e: - print "Failed to import '%s': %s" % (name, e) + except ImportError as e: + print("Failed to import '%s': %s" % (name, e)) continue fn = os.path.join(path, '%s.rst' % name) @@ -127,8 +127,8 @@ def get_documented_in_docstring(name, module=None, filename=None): return get_documented_in_lines(lines, module=name, filename=filename) except AttributeError: pass - except ImportError, e: - print "Failed to import '%s': %s" % (name, e) + except ImportError as e: + print("Failed to import '%s': %s" % (name, e)) return {} def get_documented_in_lines(lines, module=None, filename=None): diff --git a/doc/src/sphinxext/comment_eater.py b/doc/src/sphinxext/comment_eater.py index e11eea9..9c660e5 100644 --- a/doc/src/sphinxext/comment_eater.py +++ b/doc/src/sphinxext/comment_eater.py @@ -1,10 +1,10 @@ -from cStringIO import StringIO +from io import StringIO import compiler import inspect import textwrap import tokenize -from compiler_unparse import unparse +from .compiler_unparse import unparse class Comment(object): @@ -68,7 +68,7 @@ def __init__(self): def process_file(self, file): """ Process a file object. """ - for token in tokenize.generate_tokens(file.next): + for token in tokenize.generate_tokens(file.__next__): self.process_token(*token) self.make_index() diff --git a/doc/src/sphinxext/compiler_unparse.py b/doc/src/sphinxext/compiler_unparse.py index ffcf51b..bbd028d 100644 --- a/doc/src/sphinxext/compiler_unparse.py +++ b/doc/src/sphinxext/compiler_unparse.py @@ -12,11 +12,11 @@ """ import sys -import cStringIO +import io from compiler.ast import Const, Name, Tuple, Div, Mul, Sub, Add def unparse(ast, single_line_functions=False): - s = cStringIO.StringIO() + s = io.StringIO() UnparseCompilerAst(ast, s, single_line_functions) return s.getvalue().lstrip() @@ -504,7 +504,7 @@ def __binary_op(self, t, symbol): # Check if parenthesis are needed on left side and then dispatch has_paren = False left_class = str(t.left.__class__) - if (left_class in op_precedence.keys() and + if (left_class in list(op_precedence.keys()) and op_precedence[left_class] < op_precedence[str(t.__class__)]): has_paren = True if has_paren: @@ -517,7 +517,7 @@ def __binary_op(self, t, symbol): # Check if parenthesis are needed on the right side and then dispatch has_paren = False right_class = str(t.right.__class__) - if (right_class in op_precedence.keys() and + if (right_class in list(op_precedence.keys()) and op_precedence[right_class] < op_precedence[str(t.__class__)]): has_paren = True if has_paren: diff --git a/doc/src/sphinxext/docscrape.py b/doc/src/sphinxext/docscrape.py index 904270a..7cec14e 100644 --- a/doc/src/sphinxext/docscrape.py +++ b/doc/src/sphinxext/docscrape.py @@ -6,7 +6,7 @@ import textwrap import re import pydoc -from StringIO import StringIO +from io import StringIO from warnings import warn 4 class Reader(object): @@ -113,7 +113,7 @@ def __getitem__(self,key): return self._parsed_data[key] def __setitem__(self,key,val): - if not self._parsed_data.has_key(key): + if key not in self._parsed_data: warn("Unknown section %s" % key) else: self._parsed_data[key] = val @@ -369,7 +369,7 @@ def _str_index(self): idx = self['index'] out = [] out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): + for section, references in idx.items(): if section == 'default': continue out += [' :%s: %s' % (section, ', '.join(references))] @@ -411,10 +411,10 @@ def __init__(self, func, role='func'): self._role = role # e.g. "func" or "meth" try: NumpyDocString.__init__(self,inspect.getdoc(func) or '') - except ValueError, e: - print '*'*78 - print "ERROR: '%s' while parsing `%s`" % (e, self._f) - print '*'*78 + except ValueError as e: + print('*'*78) + print("ERROR: '%s' while parsing `%s`" % (e, self._f)) + print('*'*78) #print "Docstring follows:" #print doclines #print '='*78 @@ -427,7 +427,7 @@ def __init__(self, func, role='func'): argspec = inspect.formatargspec(*argspec) argspec = argspec.replace('*','\*') signature = '%s%s' % (func_name, argspec) - except TypeError, e: + except TypeError as e: signature = '%s()' % func_name self['Signature'] = signature @@ -449,8 +449,8 @@ def __str__(self): 'meth': 'method'} if self._role: - if not roles.has_key(self._role): - print "Warning: invalid role %s" % self._role + if self._role not in roles: + print("Warning: invalid role %s" % self._role) out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''), func_name) diff --git a/doc/src/sphinxext/docscrape_sphinx.py b/doc/src/sphinxext/docscrape_sphinx.py index d431ecd..d47f98d 100644 --- a/doc/src/sphinxext/docscrape_sphinx.py +++ b/doc/src/sphinxext/docscrape_sphinx.py @@ -1,5 +1,5 @@ import re, inspect, textwrap, pydoc -from docscrape import NumpyDocString, FunctionDoc, ClassDoc +from .docscrape import NumpyDocString, FunctionDoc, ClassDoc class SphinxDocString(NumpyDocString): # string conversion routines @@ -73,7 +73,7 @@ def _str_index(self): return out out += ['.. index:: %s' % idx.get('default','')] - for section, references in idx.iteritems(): + for section, references in idx.items(): if section == 'default': continue elif section == 'refguide': diff --git a/doc/src/sphinxext/numpydoc.py b/doc/src/sphinxext/numpydoc.py index 2ea41fb..f178299 100644 --- a/doc/src/sphinxext/numpydoc.py +++ b/doc/src/sphinxext/numpydoc.py @@ -17,7 +17,7 @@ """ import os, re, pydoc -from docscrape_sphinx import get_doc_object, SphinxDocString +from .docscrape_sphinx import get_doc_object, SphinxDocString import inspect def mangle_docstrings(app, what, name, obj, options, lines, @@ -44,7 +44,7 @@ def mangle_docstrings(app, what, name, obj, options, lines, try: references.append(int(l[len('.. ['):l.index(']')])) except ValueError: - print "WARNING: invalid reference in %s docstring" % name + print("WARNING: invalid reference in %s docstring" % name) # Start renaming from the biggest number, otherwise we may # overwrite references. @@ -99,7 +99,7 @@ def monkeypatch_sphinx_ext_autodoc(): if sphinx.ext.autodoc.format_signature is our_format_signature: return - print "[numpydoc] Monkeypatching sphinx.ext.autodoc ..." + print("[numpydoc] Monkeypatching sphinx.ext.autodoc ...") _original_format_signature = sphinx.ext.autodoc.format_signature sphinx.ext.autodoc.format_signature = our_format_signature diff --git a/doc/src/sphinxext/phantom_import.py b/doc/src/sphinxext/phantom_import.py index c77eeb5..7170a80 100644 --- a/doc/src/sphinxext/phantom_import.py +++ b/doc/src/sphinxext/phantom_import.py @@ -23,7 +23,7 @@ def setup(app): def initialize(app): fn = app.config.phantom_import_file if (fn and os.path.isfile(fn)): - print "[numpydoc] Phantom importing modules from", fn, "..." + print("[numpydoc] Phantom importing modules from", fn, "...") import_phantom_module(fn) #------------------------------------------------------------------------------ @@ -129,7 +129,7 @@ def base_cmp(a, b): doc = "%s%s\n\n%s" % (funcname, argspec, doc) obj = lambda: 0 obj.__argspec_is_invalid_ = True - obj.func_name = funcname + obj.__name__ = funcname obj.__name__ = name obj.__doc__ = doc if inspect.isclass(object_cache[parent]): diff --git a/doc/src/sphinxext/plot_directive.py b/doc/src/sphinxext/plot_directive.py index 6a94188..6d250c2 100644 --- a/doc/src/sphinxext/plot_directive.py +++ b/doc/src/sphinxext/plot_directive.py @@ -69,7 +69,7 @@ """ -import sys, os, glob, shutil, imp, warnings, cStringIO, re, textwrap +import sys, os, glob, shutil, imp, warnings, io, re, textwrap def setup(app): setup.app = app @@ -137,12 +137,12 @@ def run_code(code, code_path): if code_path is not None: os.chdir(os.path.dirname(code_path)) stdout = sys.stdout - sys.stdout = cStringIO.StringIO() + sys.stdout = io.StringIO() try: code = unescape_doctest(code) ns = {} - exec setup.config.plot_pre_code in ns - exec code in ns + exec(setup.config.plot_pre_code, ns) + exec(code, ns) finally: os.chdir(pwd) sys.stdout = stdout @@ -203,7 +203,7 @@ def makefig(code, code_path, output_dir, output_base, config): return i # We didn't find the files, so build them - print "-- Plotting figures %s" % output_base + print("-- Plotting figures %s" % output_base) # Clear between runs plt.close('all') @@ -311,7 +311,7 @@ def run(arguments, content, options, state_machine, state, lineno): # is it in doctest format? is_doctest = contains_doctest(code) - if options.has_key('format'): + if 'format' in options: if options['format'] == 'python': is_doctest = False else: @@ -367,7 +367,7 @@ def run(arguments, content, options, state_machine, state, lineno): return [sm] - opts = [':%s: %s' % (key, val) for key, val in options.items() + opts = [':%s: %s' % (key, val) for key, val in list(options.items()) if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] result = jinja.from_string(TEMPLATE).render( diff --git a/doc/src/sphinxext/traitsdoc.py b/doc/src/sphinxext/traitsdoc.py index e0a2d3b..298896a 100644 --- a/doc/src/sphinxext/traitsdoc.py +++ b/doc/src/sphinxext/traitsdoc.py @@ -18,13 +18,13 @@ import os import pydoc -import docscrape -import docscrape_sphinx -from docscrape_sphinx import SphinxClassDoc, SphinxFunctionDoc, SphinxDocString +from . import docscrape +from . import docscrape_sphinx +from .docscrape_sphinx import SphinxClassDoc, SphinxFunctionDoc, SphinxDocString -import numpydoc +from . import numpydoc -import comment_eater +from . import comment_eater class SphinxTraitsDoc(SphinxClassDoc): def __init__(self, cls, modulename='', func_doc=SphinxFunctionDoc): diff --git a/pylocator/GtkGLExtVTKRenderWindowInteractor.py b/pylocator/GtkGLExtVTKRenderWindowInteractor.py deleted file mode 100644 index 01f73dd..0000000 --- a/pylocator/GtkGLExtVTKRenderWindowInteractor.py +++ /dev/null @@ -1,298 +0,0 @@ -""" -This code is based on GtkVTKRenderWindowInteractor written by Prabhu -Ramachandran that ships with VTK. - -The extensions here allow the use of gtkglext rather than gtkgl and -pygtk-2 rather than pygtk-0. It requires pygtk-2.0.0 or later. - -John Hunter jdhunter@ace.bsd.uchicago.edu -""" - -import sys -import gtk -from gtk import gdk -import gtk.gtkgl -import vtk - -from shared import shared - -class GtkGLExtVTKRenderWindowInteractor(gtk.gtkgl.DrawingArea): - """ - CLASS: GtkGLExtVTKRenderWindowInteractor - DESCR: Embeds a vtkRenderWindow into a pyGTK widget and uses - vtkGenericRenderWindowInteractor for the event handling. This - class embeds the RenderWindow correctly. A __getattr__ hook is - provided that makes the class behave like a - vtkGenericRenderWindowInteractor.""" - - def __init__(self, *args): - #if shared.debug: print "GtkGLExtVTKRenderWindowInteractor.__init__()" - gtk.gtkgl.DrawingArea.__init__(self) - - self.set_double_buffered(False) - - self._RenderWindow = vtk.vtkRenderWindow() - - # private attributes - self.__Created = 0 - self._ActiveButton = 0 - - self._Iren = vtk.vtkGenericRenderWindowInteractor() - self._Iren.SetRenderWindow(self._RenderWindow) - - # mcc XXX: hmm - self._Iren.GetInteractorStyle().SetCurrentStyleToTrackballCamera() - self._Iren.AddObserver('CreateTimerEvent', self.CreateTimer) - self._Iren.AddObserver('DestroyTimerEvent', self.DestroyTimer) - self.ConnectSignals() - - # need this to be able to handle key_press events. - self.set_flags(gtk.CAN_FOCUS) - - - def set_size_request(self, w, h): - gtk.gtkgl.DrawingArea.set_size_request(self, w, h) - self._RenderWindow.SetSize(w, h) - self._Iren.SetSize(w, h) - self._Iren.ConfigureEvent() - - def ConnectSignals(self): - - self.connect("realize", self.OnRealize) - self.connect("expose_event", self.OnExpose) - self.connect("configure_event", self.OnConfigure) - self.connect("button_press_event", self.OnButtonDown) - self.connect("button_release_event", self.OnButtonUp) - self.connect("motion_notify_event", self.OnMouseMove) - self.connect("enter_notify_event", self.OnEnter) - self.connect("leave_notify_event", self.OnLeave) - self.connect("key_press_event", self.OnKeyPress) - self.connect("delete_event", self.OnDestroy) - self.add_events(gdk.EXPOSURE_MASK| - gdk.BUTTON_PRESS_MASK | - gdk.BUTTON_RELEASE_MASK | - gdk.KEY_PRESS_MASK | - gdk.POINTER_MOTION_MASK | - gdk.POINTER_MOTION_HINT_MASK | - gdk.ENTER_NOTIFY_MASK | - gdk.LEAVE_NOTIFY_MASK) - - - def __getattr__(self, attr): - """Makes the object behave like a - vtkGenericRenderWindowInteractor""" - if attr == '__vtk__': - return lambda t=self._Iren: t - elif hasattr(self._Iren, attr): - return getattr(self._Iren, attr) - else: - raise AttributeError, self.__class__.__name__ + \ - " has no attribute named " + attr - - def CreateTimer(self, obj, event): - gtk.timeout_add(10, self._Iren.TimerEvent) - - def DestroyTimer(self, obj, event): - """The timer is a one shot timer so will expire automatically.""" - return 1 - - def GetRenderWindow(self): - return self._RenderWindow - - def Render(self): - if self.__Created: - self._RenderWindow.Render() - - def OnRealize(self, *args): - if self.__Created == 0: - # you can't get the xid without the window being realized. - self.realize() - if sys.platform=='win32': - win_id = str(self.widget.window.handle) - else: - win_id = str(self.widget.window.xid) - - self._RenderWindow.SetWindowInfo(win_id) - #self._Iren.Initialize() - self.__Created = 1 - return True - - def OnConfigure(self, widget, event): - self.widget=widget - self._Iren.SetSize(event.width, event.height) - self._Iren.ConfigureEvent() - self.Render() - return True - - def OnExpose(self, *args): - self.Render() - return True - - def OnDestroy(self, event=None): - self.hide() - del self._RenderWindow - self.destroy() - return True - - def _GetCtrlShift(self, event): - ctrl, shift = 0, 0 - if ((event.state & gdk.CONTROL_MASK) == gdk.CONTROL_MASK): - ctrl = 1 - if ((event.state & gdk.SHIFT_MASK) == gdk.SHIFT_MASK): - shift = 1 - return ctrl, shift - - def OnButtonDown(self, wid, event): - if shared.debug: print "GtkGLExtVTKRenderWindowInteractor.OnButtonDown()" - """Mouse button pressed.""" - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - chr(0), 0, None) - button = event.button - if button == 3: - self._Iren.RightButtonPressEvent() - return True - elif button == 1: - self._Iren.LeftButtonPressEvent() - return True - elif button == 2: - self._Iren.MiddleButtonPressEvent() - return True - else: - return False - - def OnButtonUp(self, wid, event): - """Mouse button released.""" - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - chr(0), 0, None) - button = event.button - if button == 3: - self._Iren.RightButtonReleaseEvent() - return True - elif button == 1: - self._Iren.LeftButtonReleaseEvent() - return True - elif button == 2: - self._Iren.MiddleButtonReleaseEvent() - return True - - return False - - def OnMouseMove(self, wid, event): - """Mouse has moved.""" - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - chr(0), 0, None) - self._Iren.MouseMoveEvent() - return True - - def OnEnter(self, wid, event): - """Entering the vtkRenderWindow.""" - self.grab_focus() - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - chr(0), 0, None) - self._Iren.EnterEvent() - return True - - def OnLeave(self, wid, event): - """Leaving the vtkRenderWindow.""" - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - chr(0), 0, None) - self._Iren.LeaveEvent() - return True - - def OnKeyPress(self, wid, event): - """Key pressed.""" - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - keycode, keysym = event.keyval, event.string - key = chr(0) - if keycode < 256: - key = chr(keycode) - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - key, 0, keysym) - self._Iren.KeyPressEvent() - self._Iren.CharEvent() - return True - - def OnKeyRelease(self, wid, event): - "Key released." - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - keycode, keysym = event.keyval, event.string - key = chr(0) - if keycode < 256: - key = chr(keycode) - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - key, 0, keysym) - self._Iren.KeyReleaseEvent() - return True - - def Initialize(self): - if self.__Created: - self._Iren.Initialize() - - def SetPicker(self, picker): - self._Iren.SetPicker(picker) - - def GetPicker(self, picker): - return self._Iren.GetPicker() - -def main(): - # The main window - window = gtk.Window(gtk.WINDOW_TOPLEVEL) - window.set_title("A GtkVTKRenderWindow Demo!") - window.connect("destroy", gtk.main_quit) - window.connect("delete_event", gtk.main_quit) - window.set_border_width(10) - - # A VBox into which widgets are packed. - vbox = gtk.VBox(spacing=3) - window.add(vbox) - vbox.show() - - # The GtkVTKRenderWindow - gvtk = GtkGLExtVTKRenderWindowInteractor() - #gvtk.SetDesiredUpdateRate(1000) - gvtk.set_size_request(400, 400) - vbox.pack_start(gvtk) - gvtk.show() - gvtk.Initialize() - gvtk.Start() - # prevents 'q' from exiting the app. - gvtk.AddObserver("ExitEvent", lambda o,e,x=None: x) - - # The VTK stuff. - cone = vtk.vtkConeSource() - cone.SetResolution(80) - coneMapper = vtk.vtkPolyDataMapper() - coneMapper.SetInput(cone.GetOutput()) - #coneActor = vtk.vtkLODActor() - coneActor = vtk.vtkActor() - coneActor.SetMapper(coneMapper) - coneActor.GetProperty().SetColor(0.5, 0.5, 1.0) - ren = vtk.vtkRenderer() - gvtk.GetRenderWindow().AddRenderer(ren) - ren.AddActor(coneActor) - - # A simple quit button - quit = gtk.Button("Quit!") - quit.connect("clicked", gtk.main_quit) - vbox.pack_start(quit) - quit.show() - - # show the main window and start event processing. - window.show() - gtk.main() - - -if __name__ == "__main__": - main() diff --git a/pylocator/__init__.py b/pylocator/__init__.py index 29409c0..cefc35a 100644 --- a/pylocator/__init__.py +++ b/pylocator/__init__.py @@ -1,16 +1,6 @@ -""" -PyLocator -========= +"""Qt-based PyLocator package.""" -A program for localisation of EEG-electrodes from CT/MRI-volumes. - -Dependencies: -python -nibabel -numpy -vtk -pygtk, gtkglext -""" +__all__ = ["__version__"] __version__ = "1.0" diff --git a/pylocator/app.py b/pylocator/app.py new file mode 100644 index 0000000..fa32bfc --- /dev/null +++ b/pylocator/app.py @@ -0,0 +1,45 @@ +"""High level application helpers for PyLocator's Qt port.""" + +from __future__ import annotations + +import sys + +from PySide6.QtWidgets import QApplication + +from .controller import PyLocatorController + + +def _get_application() -> tuple[QApplication, bool]: + """Return the active :class:`QApplication` and ownership flag.""" + + existing = QApplication.instance() + if existing is not None: + return existing, False + + app = QApplication(sys.argv) + app.setApplicationName("PyLocator") + app.setOrganizationName("PyLocator Project") + return app, True + + +def run_app(initial_volume: str | None = None) -> int: + """Start the Qt application and block until it exits.""" + + app, owns_app = _get_application() + controller = PyLocatorController(parent=None) + + if initial_volume: + controller.load_volume(initial_volume) + + controller.show() + + if owns_app: + return app.exec() + + # When the caller already owns the QApplication we mimic exec()'s + # behaviour by running the event loop until the main window is closed. + # ``exec()`` is not re-entrant, therefore we simply return ``0``. + return 0 + + +__all__ = ["run_app"] diff --git a/pylocator/colors.py b/pylocator/colors.py deleted file mode 100644 index 4ba03c2..0000000 --- a/pylocator/colors.py +++ /dev/null @@ -1,197 +0,0 @@ -from __future__ import division - -import gobject -import gtk - -from gtkutils import make_option_menu - -colorSeq = ( - ( 'light skin' , (0.953, 0.875, 0.765) ), - ( 'dark skin' , (0.624, 0.427, 0.169) ), - ( 'electrodes' , (0.482, 0.737, 0.820) ), - ( 'bone' , (0.9804, 0.9216, 0.8431) ), - ) - - -colord = dict(colorSeq) - -class ColorChooser(gtk.Frame): - def __init__(self,color=None): - gtk.Frame.__init__(self) - self._ignore_updates = False - self.da = gtk.DrawingArea() - self.da.show() - self.add(self.da) - self.da.set_size_request(40,20) - if color==None: - color=gtk.gdk.Color(1.,1.,1.) - self.set_color(color) - self.set_border_width(5) - self.set_property("shadow-type",gtk.SHADOW_ETCHED_IN) - self.da.add_events(gtk.gdk.BUTTON_PRESS_MASK) - self.da.connect("button-press-event",self.choose_color) - - def _set_color(self,color,ignore_updates=True): - try: - self._ignore_updates = ignore_updates - if not type(color) == gtk.gdk.Color: - color = tuple2gdkColor(color) - for state in [gtk.STATE_NORMAL, - gtk.STATE_ACTIVE, - gtk.STATE_PRELIGHT, - gtk.STATE_SELECTED, - gtk.STATE_INSENSITIVE]: - self.da.modify_bg(state,color) - self._color=color - if not self._ignore_updates: - self.emit("color_changed") - finally: - self._ignore_updates = False - - - def set_color(self, color): - self._set_color(color, False) - - def get_color(self): - return self._color - - def choose_color(self, *args): - new_color = choose_one_color( - 'Choose color...', - self._color - ) - if new_color != self._color: - self.set_color(new_color) - - color = property(get_color,set_color) - -gobject.type_register(ColorChooser) -gobject.signal_new("color_changed", - ColorChooser, - gobject.SIGNAL_RUN_FIRST, - gobject.TYPE_NONE, ()) - -class ColorChooserWithPredefinedColors(gtk.HBox): - custom_str = "custom..." - def __init__(self, colorSeq=colorSeq): - names, self.colors= zip(*colorSeq) - self.colorDict = dict(colorSeq) - - self.colorNames = list(names) - - gtk.HBox.__init__(self) - self._ignore_updates = False - - vb1 = gtk.VBox() - vb2 = gtk.VBox() - vb1.show() - vb2.show() - self.pack_start(vb1, False) - self.pack_start(vb2, False) - - self.colorNames.append(self.custom_str) - self.optmenu = make_option_menu( - self.colorNames, self._ddlist_changed) - self.optmenu.show() - vb1.pack_start(self.optmenu, True, False) - - initial_color = self.colorDict[self.optmenu.get_active_text()] - self.color_chooser = ColorChooser(initial_color) - vb2.pack_start(self.color_chooser, True, False) - self.color_chooser.connect("color_changed", self.color_changed) - - self.show() - - def _ddlist_changed(self, item, *args): - s = item.get_active_text() - if s==self.custom_str: - self.color_chooser.show() - if not self._ignore_updates: - self.color_chooser.choose_color() - else: - self.color_chooser.hide() - if not self._ignore_updates: - self.color_chooser.set_color(tuple2gdkColor(self.colorDict[s])) - - def color_changed(self, *args): - self.emit("color_changed") - - def get_color(self): - return self.color_chooser.get_color() - - def _set_color(self,color,ignore_updates=True): - try: - self._ignore_updates=ignore_updates - if type(color)==str: - idx = self.colorNames.index(color) - if not (idx>-1 and idx None: + self._main_window = MainWindow(parent) + self._main_window.request_open_file.connect(self.select_and_load_volume) + self._last_dir: Path | None = None + + # ------------------------------------------------------------------ + # Qt window helpers + # ------------------------------------------------------------------ + def show(self) -> None: + """Show the main window.""" + + self._main_window.show() + self._main_window.raise_() + + # ------------------------------------------------------------------ + # Volume loading + # ------------------------------------------------------------------ + def select_and_load_volume(self) -> None: + """Open a file dialog and load the chosen NIfTI volume.""" + + start_dir = str(self._last_dir) if self._last_dir else str(Path.cwd()) + filename, _ = QFileDialog.getOpenFileName( + self._main_window, + "Open NIfTI volume", + start_dir, + "NIfTI files (*.nii *.nii.gz);;All files (*)", + ) + if not filename: + return + self.load_volume(filename) + + def load_volume(self, filename: str) -> bool: + """Load *filename* and display it in the VTK viewport.""" + + try: + volume = load_nifti_volume(filename) + except NiftiLoadError as exc: # pragma: no cover - UI feedback only + QMessageBox.critical( + self._main_window, + "Failed to load volume", + str(exc), + ) return False - else: - imageData = reader.GetOutput() - EventHandler().notify('set image data', imageData) - EventHandler().notify("set axes directions") - EventHandler().set_nifti(reader) - self.store_current_camera_fpus() - EventHandler().notify("render now") - return True - - def set_mouse_interact_mode(self, menuItem, *args): - """Sets the interaction mode of all marker window interactors - Uses a mapping from menu item label to command. - """ - commmands = { - "_Mouse Interact" : "mouse1 interact", - "_VTK Interact" : "vtk interact", - "Set _Label" : "mouse1 label", - "_Select Markers" : "mouse1 select", - "Set _Color" : "mouse1 color", - "_Move Markers" : "mouse1 move", - "_Delete Markers" : "mouse1 delete" - } - - if menuItem.get_active(): - menuItemLabel = menuItem.get_label() - #print menuItemLabel, commmands[menuItemLabel] - EventHandler().notify(commmands[menuItemLabel]) - - def align_planes_to_surf_view(self, *args): - fpu = self.window.surfRenWin.get_camera_fpu() - self.window.pwxyz.set_camera(fpu) - self.window.pwxyz.Render() - - def align_surf_to_planes_view(self, *args): - fpu = self.window.pwxyz.get_camera_fpu() - self.window.surfRenWin.set_camera(fpu) - self.window.surfRenWin.Render() - - def store_current_camera_fpus(self): - for rw in self.get_render_windows(): - rw.store_camera_default() - - def reset_cameras(self, *args): - for rw in self.get_render_windows(): - rw.reset_camera_to_default() - - def toggle_labels(self, *args): - eh = EventHandler() - if eh.get_labels_on(): - eh.set_labels_off() - else: - eh.set_labels_on() - - def show_settings_dialog(self,*args): - settings = SettingsController(self.window.pwxyz) - settings.dialog.run() - - def choose_color(self, *args): - dialog = gtk.ColorSelectionDialog('Choose default marker color') - colorsel = dialog.colorsel - colorsel.set_previous_color(self.lastColor) - colorsel.set_current_color(self.lastColor) - colorsel.set_has_palette(True) - response = dialog.run() - - if response == gtk.RESPONSE_OK: - color = colorsel.get_current_color() - self.lastColor = color - EventHandler().set_default_color(gdkColor2tuple(color)) - - dialog.destroy() - - - def show_about_dialog(self,item,*args): - about(pylocator.__version__) - def open_help(self,item,*args): - pass + self._last_dir = Path(filename).resolve().parent + self._main_window.display_volume(volume) + return True - def __update_title_of_window(self, filename): - pass +__all__ = ["PyLocatorController"] diff --git a/pylocator/decimate_filter.py b/pylocator/decimate_filter.py deleted file mode 100644 index 75e1209..0000000 --- a/pylocator/decimate_filter.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import division -import sys, os -import vtk - -import gtk - -from gtkutils import ProgressBarDialog, str2posnum_or_err - - -# vtkDecimate is patented and no longer in VTK5. we will try vtkDecimatePro (argh) -# class DecimateFilter(vtk.vtkDecimate): -class DecimateFilter(vtk.vtkDecimatePro): - """ - CLASS: DecimateFilter - DESCR: - - Public attrs: - targetReduction - #aspectRatio - #initialError - #errorIncrement - #maxIterations - #initialAngle - """ - - fmts = { - 'targetReduction' : '%1.2f', - #'initialError' : '%1.5f', - #'errorIncrement' : '%1.4f', - #'maxIterations' : '%d', - #'initialAngle' : '%1.1f', - } - - - labels = { - 'targetReduction' : 'Target reduction', - #'initialError' : 'Initial error', - #'errorIncrement' : 'Error increment', - #'maxIterations' : 'Maximum iterations', - #'initialAngle' :'Initial angle', - } - - converters = { - 'targetReduction' : str2posnum_or_err, - #'initialError' : str2posnum_or_err, - #'errorIncrement' : str2posnum_or_err, - #'maxIterations' : str2posint_or_err, - #'initialAngle' : str2posnum_or_err, - - } - targetReduction = 0.8 - #initialError = 0.0005 - #errorIncrement = 0.001 - #maxIterations = 6 - #initialAngle = 30 - - def __init__(self): - prog = ProgressBarDialog( - title='Rendering surface', - parent=None, - msg='Decimating data....', - size=(300,40), - ) - prog.set_modal(True) - - def start(o, event): - prog.show() - while gtk.events_pending(): gtk.main_iteration() - - - def progress(o, event): - val = o.GetProgress() - prog.bar.set_fraction(val) - while gtk.events_pending(): gtk.main_iteration() - - def end(o, event): - prog.hide() - while gtk.events_pending(): gtk.main_iteration() - - self.AddObserver('StartEvent', start) - self.AddObserver('ProgressEvent', progress) - self.AddObserver('EndEvent', end) - - def update(self): - self.SetTargetReduction(self.targetReduction) - #self.SetInitialError(self.initialError) - #self.SetErrorIncrement(self.errorIncrement) - #self.SetMaximumIterations(self.maxIterations) - #self.SetInitialFeatureAngle(self.initialAngle) - diff --git a/pylocator/dialogs.py b/pylocator/dialogs.py deleted file mode 100644 index 5e0342b..0000000 --- a/pylocator/dialogs.py +++ /dev/null @@ -1,159 +0,0 @@ -import gtk -import re -from resources import edit_label_dialog, edit_coordinates_dialog, edit_settings_dialog, about_dialog -from gtkutils import str2num_or_err -from colors import gdkColor2tuple, tuple2gdkColor -from events import EventHandler -from shared import shared - - -def edit_label(oldLabel="", description=None): - builder = gtk.Builder() - builder.add_from_file(edit_label_dialog) - dialog = builder.get_object("dialog") - entry = builder.get_object("entry") - entry.set_text(oldLabel) - if description!=None: - label1 = builder.get_object("label1") - label1.set_text(description) - - response = dialog.run() - - if response==gtk.RESPONSE_OK: - label = entry.get_text() - else: - label = None - - dialog.destroy() - return label - -def edit_label_of_marker(marker): - label = marker.get_label() - defaultLabel = label - print defaultLabel, shared.lastLabel - if defaultLabel=='' and shared.lastLabel is not None: - m = re.match('(.+?)(\d+)', shared.lastLabel) - if m: - num = str(int(m.group(2))+1).zfill(len(m.group(2))) - defaultLabel = m.group(1) + num - print defaultLabel - - new_label = edit_label(defaultLabel) - if shared.debug: print new_label, label - - if new_label==None or new_label==label: return - EventHandler().notify('label marker', marker, new_label) - shared.lastLabel = new_label - - -def edit_coordinates(X=0,Y=0,Z=0, description=None): - builder = gtk.Builder() - builder.add_from_file(edit_coordinates_dialog) - dialog = builder.get_object("dialog") - labelDescription = builder.get_object("labelDescription") - entryX = builder.get_object("entry1") - entryY = builder.get_object("entry2") - entryZ = builder.get_object("entry3") - - if description!=None: - labelDescription.set_text(description) - - for ax, entry in zip((X,Y,Z),(entryX,entryY,entryZ)): - entry.set_text("%.3f"%ax) - - while 1: - response = dialog.run() - - if response==gtk.RESPONSE_OK: - val1 = str2num_or_err(entryX.get_text(), "X", None) - if val1 is None: continue - val2 = str2num_or_err(entryY.get_text(), "Y", None) - if val2 is None: continue - val3 = str2num_or_err(entryZ.get_text(), "Z", None) - if val3 is None: continue - - rv= val1, val2, val3 - else: rv = None - break - - dialog.destroy() - return rv - -def about(version="0.xyz"): - builder = gtk.Builder() - builder.add_from_file(about_dialog) - dialog = builder.get_object("dialog") - label = builder.get_object("label2") - - label.set_text(label.get_text().replace("__version__",version)) - - dialog.run() - - dialog.destroy() - return label - - - -class SettingsController(object): - def __init__(self, pwxyz): - self.pwxyz = pwxyz - - builder = gtk.Builder() - builder.add_from_file(edit_settings_dialog) - - self.dialog = builder.get_object("dialog") - self.mo = builder.get_object("marker_opacity") - self.ms = builder.get_object("marker_size") - self.po = builder.get_object("planes_opacity") - self.dc = builder.get_object("colorbutton") - - self.__get_current_values() - - builder.connect_signals(self) - - def __get_current_values(self): - self.po.set_value(shared.planes_opacity) - - self.mo.set_value(shared.markers_opacity) - self.ms.set_value(shared.marker_size) - - old_color = EventHandler().get_default_color() - self.dc.set_color(tuple2gdkColor(old_color)) - - def marker_opacity_changed(self, *args): - val = self.mo.get_value() - for marker in EventHandler().get_markers_as_seq(): - marker.GetProperty().SetOpacity(val) - shared.marker_opacity = val - EventHandler().notify("render now") - - def marker_size_changed(self, *args): - val = self.ms.get_value() - for marker in EventHandler().get_markers_as_seq(): - marker.set_size(val) - shared.marker_size = val - EventHandler().notify("render now") - - def planes_opacity_changed(self, *args): - val = self.po.get_value() - for pw in self.__get_plane_widgets(): - pw.GetTexturePlaneProperty().SetOpacity(val) - pw.GetPlaneProperty().SetOpacity(val) - shared.planes_opacity = val - self.pwxyz.Render() - - def set_default_color(self, *args): - color = self.dc.get_color() - EventHandler().set_default_color(gdkColor2tuple(color)) - - def close_dialog(self, *args): - print "close dialog" - self.dialog.hide() - self.dialog.destroy() - - def __get_plane_widgets(self): - pwx = self.pwxyz.pwX - pwy = self.pwxyz.pwY - pwz = self.pwxyz.pwZ - return pwx, pwy, pwz - diff --git a/pylocator/events.py b/pylocator/events.py deleted file mode 100644 index bfe7744..0000000 --- a/pylocator/events.py +++ /dev/null @@ -1,199 +0,0 @@ -import vtk -from markers import Marker -import pickle -from shared import shared -from vtkutils import vtkmatrix4x4_to_array - -class UndoRegistry: - __sharedState = {} - commands = [] - lastPop = None, [] - - def __init__(self): - self.__dict__ = self.__sharedState - - def push_command(self, func, *args): - self.commands.append((func, args)) - - def undo(self): - if len(self.commands)==0: return - func, args = self.commands.pop() - self.lastPop = func, args - func(*args) - - - def flush(self): - self.commands = [] - - def get_last_pop(self): - return self.lastPop - -class EventHandler: - __sharedState = {} - markers = vtk.vtkActorCollection() - defaultColor = (0,0,1.) - labelsOn = 1 - observers = {} - selected = {} - __NiftiQForm=None - __NiftiSpacings=(1.0,1.0,1.0) - __NiftiShape=None - __NiftiMin = None - __NiftiMax = None - __NiftiMedian = None - - def __init__(self): - self.__dict__ = self.__sharedState - - def add_selection(self, marker): - self.selected[marker] = 1 - self.notify('select marker', marker) - - def remove_selection(self, marker): - if self.selected.has_key(marker): - del self.selected[marker] - self.notify('unselect marker', marker) - - def clear_selection(self): - for oldMarker in self.selected.keys(): - self.remove_selection(oldMarker) - - def select_new(self, marker): - self.clear_selection() - self.add_selection(marker) - - def add_marker(self, marker): - # break undo cycle - func, args = UndoRegistry().get_last_pop() - #if shared.debug: print 'add', func, args - if len(args)==0 or \ - (func, args[0]) != (self.add_marker, marker): - UndoRegistry().push_command(self.remove_marker, marker) - self.markers.AddItem(marker) - self.notify('add marker', marker) - - - def remove_marker(self, marker): - # break undo cycle - - func, args = UndoRegistry().get_last_pop() - #if shared.debug: print 'remove', func, args - if len(args)==0 or \ - (func, args[0]) != (self.remove_marker, marker): - UndoRegistry().push_command(self.add_marker, marker) - self.markers.RemoveItem(marker) - self.notify('remove marker', marker) - - def get_markers(self): - return self.markers - - def get_markers_as_seq(self): - numMarkers = self.markers.GetNumberOfItems() - self.markers.InitTraversal() - return [self.markers.GetNextActor() for i in range(numMarkers)] - - - def set_default_color(self, color): - self.defaultColor = color - - def get_default_color(self): - return self.defaultColor - - - def save_markers_as(self, fname): - self.markers.InitTraversal() - numMarkers = self.markers.GetNumberOfItems() - lines = []; - - for i in range(numMarkers): - marker = self.markers.GetNextActor() - if marker is None: continue - else: - lines.append(marker.to_string()) - lines.sort() - - fh = file(fname, 'w') - fh.write('\n'.join(lines) + '\n') - - def set_nifti(self,reader): - reader.GetQForm(),reader.nifti_voxdim,reader.shape - self.__NiftiQForm=reader.GetQForm() - self.__NiftiSpacings=reader.nifti_voxdim - self.__NiftiShape=reader.shape - self.__NiftiMin = reader.min - self.__NiftiMax = reader.max - self.__NiftiMedian = reader.median - - def get_nifti_stats(self): - return (self.__NiftiMin, - self.__NiftiMedian, - self.__NiftiMax) - - def set_vtkactor(self, vtkactor): - if shared.debug: print "EventHandler.set_vtkactor()" - self.vtkactor = vtkactor - - def save_registration_as(self, fname): - if shared.debug: print "EventHandler.save_registration_as(", fname,")" - fh = file(fname, 'w') - - # XXX mcc: somehow get the transform for the VTK actor. aiieeee - #xform = self.vtkactor.GetUserTransform() - loc = self.vtkactor.GetOrigin() - pos = self.vtkactor.GetPosition() - scale = self.vtkactor.GetScale() - mat = self.vtkactor.GetMatrix() - orient = self.vtkactor.GetOrientation() - - if shared.debug: print "EventHandler.save_registration_as(): vtkactor has origin, pos, scale, mat, orient=", loc, pos, scale, mat, orient, "!!" - - scipy_mat = vtkmatrix4x4_to_array(mat) - - pickle.dump(scipy_mat, fh) - fh.close() - - - def load_markers_from(self, fname): - self.notify('render off') - for line in file(fname, 'r'): - marker = Marker.from_string(line) - self.add_marker(marker) - self.notify('render on') - UndoRegistry().flush() - - def attach(self, observer): - self.observers[observer] = 1 - - def detach(self, observer): - try: - del self.observers[observer] - except KeyError: pass - - def notify(self, event, *args): - for observer in self.observers.keys(): - if shared.debug: - print "EventHandler.notify(", event, "): calling update_viewer for ", observer - try: - observer.update_viewer(event, *args) - except Exception, e: - print "Error while updating observer", observer, type(e), e - - def get_labels_on(self): - return self.labelsOn - - def set_labels_on(self): - self.labelsOn = 1 - self.notify('labels on') - - def set_labels_off(self): - self.labelsOn = 0 - self.notify('labels off') - - def is_selected(self, marker): - return self.selected.has_key(marker) - - def get_selected(self): - return self.selected.keys() - - def get_num_selected(self): - return len(self.selected) diff --git a/pylocator/gtkutils.py b/pylocator/gtkutils.py deleted file mode 100644 index 27ed038..0000000 --- a/pylocator/gtkutils.py +++ /dev/null @@ -1,1009 +0,0 @@ -import os, sys -import StringIO, traceback - -import gobject, gtk -from gtk import gdk -from shared import shared -import datetime - -def is_string_like(obj): - if hasattr(obj, 'shape'): return 0 # this is a workaround - # for a bug in numeric<23.1 - try: obj + '' - except (TypeError, ValueError): return 0 - return 1 - -def exception_to_str(s = None): - - sh = StringIO.StringIO() - if s is not None: print >>sh, s - traceback.print_exc(file=sh) - return sh.getvalue() - - - -def donothing_callback(*args): - pass - -class ProgressBarDialog(gtk.Dialog): - "Use attribute bar to control the progress bar" - def __init__(self, title, parent, msg='Almost there....', size=(300, 40)): - gtk.Dialog.__init__(self, title=title, flags=gtk.DIALOG_MODAL) - - if parent is not None: - self.set_transient_for(parent) - - self.bar = gtk.ProgressBar() - self.bar.set_size_request(size[0], size[1]) - - self.bar.set_text(msg) - self.bar.set_fraction(0) - self.bar.show() - self.vbox.pack_start(self.bar) - -def raise_msg_to_str(msg): - """msg is a return arg from a raise. Join with new lines""" - if not is_string_like(msg): - msg = '\n'.join(map(str, msg)) - return msg - -def error_msg(msg, parent=None, title=None): - dialog = gtk.MessageDialog( - parent = None, - type = gtk.MESSAGE_ERROR, - buttons = gtk.BUTTONS_OK, - message_format = msg) - if parent is not None: - dialog.set_transient_for(parent) - if title is not None: - dialog.set_title(title) - else: - dialog.set_title('Error!') - dialog.show() - dialog.run() - dialog.destroy() - return None - -def simple_msg(msg, parent=None, title=None): - dialog = gtk.MessageDialog( - parent = None, - type = gtk.MESSAGE_INFO, - buttons = gtk.BUTTONS_OK, - message_format = msg) - if parent is not None: - dialog.set_transient_for(parent) - if title is not None: - dialog.set_title(title) - dialog.show() - dialog.run() - dialog.destroy() - return None - -def _get_label(l): - if type(l)==type(''): return l - else: return l.get_label() - -def str2num_or_err(s, label, parent=None): - "label can be a string or label widget" - label = _get_label(label) - try: return float(s) - except ValueError: - return error_msg('%s entry must be a number; you entered "%s"' % - (label, s), parent) - -def str2posnum_or_err(s, labelWidget, parent=None): - label = _get_label(labelWidget) - val = str2num_or_err(s, labelWidget, parent) - if val > 0 or val is None: return val - - msg = '%s must be a positive number.\nYou supplied "%s"' %\ - (label, s) - - return error_msg(msg, parent) - -def str2negnum_or_err(s, labelWidget, parent=None): - label = _get_label(labelWidget) - val = str2num_or_err(s, labelWidget, parent) - if val < 0 or val is None: return val - - msg = '%s must be a positive number.\nYou supplied "%s"' %\ - (label, s) - - return error_msg(msg, parent) - -def str2int_or_err(s, labelWidget, parent=None): - label = _get_label(labelWidget) - try: return int(s) - except ValueError: - if s.find('0x')==0: # looks like hex - try: return int(s, 16) - except ValueError: pass - except TypeError: pass - msg = '%s must be an integer.\nYou supplied "%s"' %\ - (label, s) - - return error_msg(msg, parent) - -def str2posint_or_err(s, labelWidget, parent=None): - label = _get_label(labelWidget) - val = str2int_or_err(s, labelWidget, parent) - if val > 0 or val is None: return val - - msg = '%s must be a positive integer.\nYou supplied "%s"' %\ - (label, s) - - return error_msg(msg, parent) - -def str2negint_or_err(s, labelWidget, parent=None): - label = _get_label(labelWidget) - val = str2int_or_err(s, labelWidget, parent) - if val < 0 or val is None: return val - - msg = '%s must be a negative integer.\nYou supplied "%s"' %\ - (label, s) - - return error_msg(msg, parent) - - - -class Dialog_FileSelection(gtk.FileSelection): - - def __init__(self, defaultDir, okCallback, title='Select file', - parent=None): - """wrap some of the file selection boilerplate. okCallback is - a function that takes a Dialog_FileSelection instance as a - single arg.""" - if shared.debug: print "Dialog_FileSelection.__init__" - self.defaultDir = defaultDir - self.okCallback = okCallback - gtk.FileSelection.__init__(self, title=title) - self.set_filename(defaultDir + os.sep) - self.connect("destroy", lambda w: self.destroy()) - - self.ok_button.connect("clicked", self.file_ok_sel) - self.cancel_button.connect("clicked", - lambda *args: self.destroy()) - if parent is not None: - self.set_transient_for(parent) - self.show() - - def get_default_dir(self): - return self.defaultDir - - def file_ok_sel(self, w): - filename = self.get_filename() - (path, fname) = os.path.split(filename) - self.defaultDir = path - self.okCallback(self) - - -class Dialog_DirSelection(gtk.FileSelection): - - def __init__(self, defaultDir, okCallback, title='Select directory'): - """ - A file selection dialog that forces the user to choose a - directory. - - okCallback is a function that takes a Dialog_DirSelection - instance as a single arg. - - """ - - self.defaultDir = defaultDir - self.okCallback = okCallback - gtk.FileSelection.__init__(self, title=title) - self.set_filename(defaultDir + os.sep) - self.connect("destroy", lambda w: self.destroy()) - - self.ok_button.connect("clicked", self.file_ok_sel) - self.cancel_button.connect("clicked", - lambda w: self.destroy()) - self.show() - - def get_default_dir(self): - return self.defaultDir - - def file_ok_sel(self, w): - thisDir = self.get_filename() - if not os.path.isdir(thisDir): - simple_msg( - 'You must select a directory\n%s is not a dir' % - thisDir) - return - - self.defaultDir = thisDir - self.okCallback(self) - - - -def ignore_or_act(msg, actionCallback, title='Ignore?', parent=None): - - - d = gtk.Dialog(title, flags=gtk.DIALOG_MODAL) - l = gtk.Label(msg) - l.show() - d.vbox.pack_start(l) - - if parent is not None: - d.set_transient_for(parent) - - def destroy_callback(*args): - d.destroy() - - b = gtk.Button('Ignore') - b.connect('clicked', destroy_callback) - b.show() - d.vbox.pack_start(b) - - b = gtk.Button('Fix') - b.connect('clicked', actionCallback) - b.show() - d.vbox.pack_start(b) - - d.show() - - - -def not_implemented(parent=None, *args): - "Popup a message for a widget that doesn't have a callback implemented yet" - - simple_msg('Not implemented yet; sorry!', - title='Error: Feature Unimplemented', - parent=parent) - -def yes_or_no(msg, title, responseCallback, parent=None): - """ - Pop up a yes or no dialog. A typical response callback would look like - - def response(dialog, response): - if response==gtk.RESPONSE_YES: - if shared.debug: print 'yes, yes!' - elif response==gtk.RESPONSE_NO: - if shared.debug: print 'oh no!' - else: - if shared.debug: print 'I am deeply confused' - dialog.destroy() - """ - - dialog = gtk.MessageDialog( - parent = parent, - flags = gtk.DIALOG_DESTROY_WITH_PARENT, - type = gtk.MESSAGE_INFO, - buttons = gtk.BUTTONS_YES_NO, - message_format = msg) - - dialog.set_title(title) - dialog.connect('response', responseCallback) - dialog.show() - - -def make_option_menu_from_strings(keys): - - menu = gtk.Menu() - itemd = {} - for k in keys: - menuItem = gtk.MenuItem(k) - menuItem.show() - menu.append(menuItem) - itemd[menuItem] = k - return menu, itemd - -class FileManager: - if sys.platform=='win32': - last = 'C:\\' - else: - last = os.getcwd() - - def __init__(self, parent=None): - self.parent = parent - self.additionalWidget = None - - - def get_lastdir(self): - return self.last - - def set_lastdir(self, s): - if os.path.isdir(s): - self.last = s - elif os.path.isfile(s): - basedir, fname = os.path.split(s) - self.last = basedir - else: pass - - def get_filename(self, fname=None, title='Select file name', parent=None): - dlg = gtk.FileSelection(title) - - if (self.additionalWidget): - self.additionalWidget.show_all() - dlg.action_area.add(self.additionalWidget) - - if parent is not None: - dlg.set_transient_for(parent) - elif self.parent is not None: - dlg.set_transient_for(self.parent) - - if fname is None: - dlg.set_filename(self.get_lastdir() + os.sep) - else: - dlg.set_filename(fname) - - dlg.cancel_button.connect("clicked", lambda w: dlg.destroy()) - dlg.show() - - response = dlg.run() - - if response == gtk.RESPONSE_OK: - fullpath = dlg.get_filename() - self.set_lastdir(fullpath) - dlg.destroy() - return fullpath - - self.additionalWidget = None - return None - - def add_widget(self, widget): - self.additionalWidget = widget - - -def get_num_range(minLabel='Min', maxLabel='Max', - title='Enter range', parent=None, as_times=False): - 'Get a min, max numeric range' - dlg = gtk.Dialog(title) - if parent is not None: - dlg.set_transient_for(parent) - vbox = dlg.vbox - - labelMin = gtk.Label(minLabel) - labelMin.show() - - labelMax = gtk.Label(maxLabel) - labelMax.show() - - entryMin = gtk.Entry() - entryMin.show() - entryMin.set_width_chars(10) - - entryMax = gtk.Entry() - entryMax.show() - entryMax.set_width_chars(10) - entryMax.set_activates_default(True) - - table = gtk.Table(2,2) - table.show() - table.set_row_spacings(4) - table.set_col_spacings(4) - - table.attach(labelMin, 0, 1, 0, 1) - table.attach(labelMax, 1, 2, 0, 1) - table.attach(entryMin, 0, 1, 1, 2) - table.attach(entryMax, 1, 2, 1, 2) - dlg.vbox.pack_start(table, True, True) - - dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - dlg.set_default_response(gtk.RESPONSE_OK) - - dlg.show() - - while 1: - response = dlg.run() - - if response==gtk.RESPONSE_OK: - if (as_times): - # mcc XXX: what's the magic code word to unfurl an array into a tuple or untupled comma-separated variables? - x= map(int, (entryMin.get_text()).split(':')) - try: - minVal = datetime.time(x[0], x[1], x[2]) - except ValueError: - msg = exception_to_str('ValueError: minVal not in HH:MM:SS format') - if shared.debug: print "get_num_range (as_times=True): minVal = " , str(minVal) - else: - minVal = str2num_or_err(entryMin.get_text(), labelMin, parent) - if minVal is None: continue - if (as_times): - x= map(int, (entryMax.get_text()).split(':')) - try: - maxVal = datetime.time(x[0], x[1], x[2]) - except ValueError: - msg = exception_to_str('ValueError: maxVal not in HH:MM:SS format') - if shared.debug: print "get_num_range (as_times=True): maxVal = " , str(maxVal) - else: - maxVal = str2num_or_err(entryMax.get_text(), labelMax, parent) - if maxVal is None: continue - - if minVal>maxVal: - msg = '%s entry must be greater than %s entry' % \ - (maxLabel, minLabel) - error_msg(msg, parent, title='Invalid Entries') - continue - dlg.destroy() - return minVal, maxVal - else: - dlg.destroy() - return None - - - -def select_name(names, title='Select Name'): - 'Use radio buttons to select from a list of names' - dlg = gtk.Dialog(title) - - vbox = dlg.vbox - - - buttond = {} - buttons = [] - for name in names: - if len(buttons): - button = gtk.RadioButton(buttons[0]) - else: - button = gtk.RadioButton(None) - buttons.append(button) - button.set_label(name) - button.show() - vbox.pack_start(button, True, True) - buttond[button] = name - hbox = gtk.HBox() - hbox.show() - vbox.pack_start(hbox, False, False) - - dlg.add_button('Cancel', gtk.RESPONSE_CANCEL) - dlg.add_button('OK', gtk.RESPONSE_OK) - dlg.show() - - response = dlg.run() - - if response == gtk.RESPONSE_OK: - for button, name in buttond.items(): - if button.get_active(): - dlg.destroy() - return name - dlg.destroy() - return None - - -def make_option_menu( names, func=None ): - """ - Make an option menu with list of names in names. Return value is - a optMenu, itemDict tuple, where optMenu is the option menu and - itemDict is a dictionary mapping menu items to labels. Eg - - optmenu, menud = make_option_menu( ('Bill', 'Ted', 'Fred') ) - - ...set up dialog ... - if response==gtk.RESPONSE_OK: - item = optmenu.get_menu().get_active() - if shared.debug: print menud[item] # this is the selected name - - - if func is not None, call func with menuitem and label when - selected; eg the signature of func is - - def func(menuitem, s): - pass - """ - #optmenu = gtk.OptionMenu() - #optmenu.show() - #menu = gtk.Menu() - #menu.show() - #d = {} - #for label in names: - # item = gtk.MenuItem(label) - # menu.append(item) - # item.show() - # d[item] = label - # if func is not None: - # item.connect("activate", func, label) - #optmenu.set_menu(menu) - #return optmenu, d - - combobox = gtk.combo_box_new_text() - for label in names: - combobox.append_text(label) - combobox.set_active(0) - combobox.connect('changed', func) - combobox.show_all() - return combobox - - -def get_num_value(labelStr='Value', title='Enter value', parent=None, - default=None): - 'Get a numeric value' - dlg = gtk.Dialog(title) - if parent is not None: - dlg.set_transient_for(parent) - vbox = dlg.vbox - - label = gtk.Label(labelStr) - label.show() - - - entry = gtk.Entry() - entry.show() - entry.set_width_chars(10) - entry.set_activates_default(True) - if default is not None: - entry.set_text('%1.4f' % default) - - hbox = gtk.HBox() - hbox.show() - hbox.pack_start(label, True, True) - hbox.pack_start(entry, True, True) - - dlg.vbox.pack_start(hbox, True, True) - - dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - dlg.set_default_response(gtk.RESPONSE_OK) - - dlg.show() - - while 1: - response = dlg.run() - - if response==gtk.RESPONSE_OK: - val = str2num_or_err(entry.get_text(), label, parent) - if val is None: continue - dlg.destroy() - return val - else: - dlg.destroy() - return None - - -def get_two_nums(label1Str='Min', label2Str='Max', - title='Enter numbers', parent=None, - tooltip1=None, tooltip2=None): - 'Get two numeric values' - dlg = gtk.Dialog(title) - if parent is not None: - dlg.set_transient_for(parent) - vbox = dlg.vbox - - label1 = gtk.Label(label1Str) - label1.show() - - label2 = gtk.Label(label2Str) - label2.show() - - entry1 = gtk.Entry() - entry1.show() - entry1.set_width_chars(10) - - entry2 = gtk.Entry() - entry2.show() - entry2.set_width_chars(10) - entry2.set_activates_default(True) - - table = gtk.Table(2,2) - table.show() - table.set_row_spacings(4) - table.set_col_spacings(4) - - table.attach(label1, 0, 1, 0, 1) - table.attach(label2, 1, 2, 0, 1) - table.attach(entry1, 0, 1, 1, 2) - table.attach(entry2, 1, 2, 1, 2) - dlg.vbox.pack_start(table, True, True) - - dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - dlg.set_default_response(gtk.RESPONSE_OK) - - dlg.show() - - while 1: - response = dlg.run() - - if response==gtk.RESPONSE_OK: - val1 = str2num_or_err(entry1.get_text(), label1, parent) - if val1 is None: continue - val2 = str2num_or_err(entry2.get_text(), label2, parent) - if val2 is None: continue - - dlg.destroy() - return val1, val2 - else: return None - -def get_three_nums(label1Str='Value 1', label2Str='Value 2', - label3Str='Value 3', - value1="", value2="",value3="", - title='Enter numbers', parent=None, - tooltip1=None, tooltip2=None, tooltip3=None): - 'Get three numeric values' - - def make_float_string(value): - try: - rv = "%.2f"%float(value) - return rv - except Exception, e: - return str(value) - - dlg = gtk.Dialog(title) - if parent is not None: - print "parent not None:", parent - dlg.set_transient_for(parent) - vbox = dlg.vbox - - label1 = gtk.Label(label1Str) - label1.show() - - label2 = gtk.Label(label2Str) - label2.show() - - label3 = gtk.Label(label3Str) - label3.show() - - entry1 = gtk.Entry() - entry1.set_text(make_float_string(value1)) - entry1.show() - entry1.set_width_chars(10) - - entry2 = gtk.Entry() - entry2.set_text(make_float_string(value2)) - entry2.show() - entry2.set_width_chars(10) - - entry3 = gtk.Entry() - entry3.set_text(make_float_string(value3)) - entry3.show() - entry3.set_width_chars(10) - entry3.set_activates_default(True) - - table = gtk.Table(3,2) - table.show() - table.set_row_spacings(4) - table.set_col_spacings(4) - - table.attach(label1, 0, 1, 0, 1) - table.attach(label2, 1, 2, 0, 1) - table.attach(label3, 2, 3, 0, 1) - table.attach(entry1, 0, 1, 1, 2) - table.attach(entry2, 1, 2, 1, 2) - table.attach(entry3, 2, 3, 1, 2) - dlg.vbox.pack_start(table, True, True) - - dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) - dlg.set_default_response(gtk.RESPONSE_OK) - - dlg.show() - - while 1: - response = dlg.run() - - if response==gtk.RESPONSE_OK: - val1 = str2num_or_err(entry1.get_text(), label1, parent) - if val1 is None: continue - val2 = str2num_or_err(entry2.get_text(), label2, parent) - if val2 is None: continue - val3 = str2num_or_err(entry3.get_text(), label3, parent) - if val3 is None: continue - - rv= val1, val2, val3 - else: rv = None - dlg.destroy() - return rv - - -def add_button_icon_pixmap(button, pixmap, orientation='left'): - button.realize() - label = gtk.Label(button.get_children()[0].get()) - button.remove(button.get_children()[0]) - - if orientation is None: - box = gtk.HBox(spacing=0) - box.pack_start(pixmap, False, False, 0) - - if orientation in ('left', 'right'): - box = gtk.HBox(spacing=5) - elif orientation in ('top', 'bottom'): - box = gtk.VBox(spacing=5) - if orientation in ('left', 'top'): - box.pack_start(pixmap, False, False, 0) - box.pack_start(label, False, False, 0) - elif orientation in ('right', 'bottom'): - box.pack_start(label, False, False, 0) - box.pack_start(pixmap, False, False, 0) - - hbox = gtk.HBox() - if box is not None: - hbox.pack_start(box, True, False, 0) - hbox.show_all() - button.add(hbox) - -def add_button_icon(button, file, orientation='left'): - button.realize() - window = button.get_parent_window() - xpm, mask = gtk.create_pixmap_from_xpm(window, None, file) - pixmap = gtk.Image() - pixmap.set_from_pixmap(xpm, mask) - add_button_icon_pixmap(button, pixmap, orientation) - - -class OpenSaveSaveAsHBox(gtk.HBox): - - - def __init__(self, fmanager, openhook=None, savehook=None, parent=None): - """ - fmanager is a FileManager instance - - openhook is a function with signature ok = openhook(fh) - savehook is a function with signature ok = savehook(fh) - """ - gtk.HBox.__init__(self) - self.set_spacing(3) - self.fmanager = fmanager - self.openhook = openhook - self.savehook = savehook - self.parentWin = parent - self.filename = None - - label = gtk.Label('File') - label.show() - self.pack_start(label, False, False) - - button = gtk.Button(stock=gtk.STOCK_OPEN) - button.show() - button.connect('clicked', self.open) - self.pack_start(button, True, True) - - button = gtk.Button(stock=gtk.STOCK_SAVE) - button.show() - button.connect('clicked', self.save) - self.pack_start(button, True, True) - - - button = gtk.Button(stock=gtk.STOCK_SAVE_AS) - button.show() - button.connect('clicked', self.save_as) - self.pack_start(button, True, True) - - - def open(self, button): - filename = self.fmanager.get_filename(title='Select input file') - if filename is not None: - try: infile = file(filename, 'r') - except IOError, msg: - msg = exception_to_str('Could not open %s' % filename) - error_msg(msg, parent=self.parentWin) - else: - self.filename = filename - if self.openhook is not None: - ok = self.openhook(infile) - - - def save_as(self, button): - filename = self.fmanager.get_filename( - title='Select filename to save to') - if filename is None: return - self.save(button=None, filename=filename) - - - def save(self, button, **kwargs): - - filename = kwargs.get('filename', self.filename) - if filename is None: filename = self.fmanager.get_filename( - title='Save to filename') - if filename is None: return - - try: - outfile = file(filename, 'w') - except IOError, msg: - msg = exception_to_str('Could not write markers to %s' % filename) - error_msg(msg, parent=self.parentWin) - return - else: - if self.savehook is not None: - ok = self.savehook(outfile) - if ok: - self.filename = filename - simple_msg('Saved markers to %s' % filename, - parent=self.parentWin) - - -class ButtonAltLabel(gtk.Button): - """ - Use a gtk stock button with alternative label - """ - def __init__(self, labelStr, stock): - 'label is a string and stock is a gtk.STOCK_* ID' - gtk.Button.__init__(self, stock=stock) - alignment = self.get_children()[0] - hbox = alignment.get_children()[0] - image, label = hbox.get_children() - label.set_text(labelStr) - - -class SpreadSheet(gtk.Window): - """ -Example usage - -data = ( - ('First', 'Last', 'Age', 'Weight'), - ('John', 'Hunter', '33', '165'), - ('Miriam', 'Sierig', '56', '187'), - ) -sheet = SpreadSheet(data) -sheet.show_all() -gtk.main() - - """ - def __init__(self, rows, fmanager, title='Spreadsheet'): - gtk.Window.__init__(self) - - self.rows = rows - self.fmanager = fmanager - self.numRows = len(rows) - self.numCols = len(rows[0]) - - self.set_title(title) - self.set_border_width(8) - - vbox = gtk.VBox(False, 8) - self.add(vbox) - - # todo add toolbar here - toolbar = self.make_toolbar() - vbox.pack_start(toolbar, False, False) - - sw = gtk.ScrolledWindow() - sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) - sw.set_policy(gtk.POLICY_NEVER, - gtk.POLICY_AUTOMATIC) - vbox.pack_start(sw, True, True) - - model = self.create_model() - - self.treeview = gtk.TreeView(model) - self.treeview.set_rules_hint(True) - sw.add(self.treeview) - - self.add_columns() - - self.set_default_size(600, 600) - - self.add_events(gdk.BUTTON_PRESS_MASK | - gdk.KEY_PRESS_MASK| - gdk.KEY_RELEASE_MASK) - - - - - def add_columns(self): - model = self.treeview.get_model() - renderer = gtk.CellRendererText() - - for i in range(self.numCols): - column = gtk.TreeViewColumn('%d'%i, gtk.CellRendererText(), text=i) - self.treeview.append_column(column) - - def create_model(self): - types = [gobject.TYPE_STRING]*self.numCols - store = gtk.ListStore(*types) - - for row in self.rows: - iter = store.append() - pairs = [] - for i, entry in enumerate(row): pairs.extend((i, entry)) - store.set(iter, *pairs) - return store - - - def make_toolbar(self): - - toolbar = gtk.Toolbar() - iconSize = gtk.ICON_SIZE_SMALL_TOOLBAR - toolbar.set_border_width(5) - toolbar.set_style(gtk.TOOLBAR_ICONS) - toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL) - - - iconw = gtk.Image() # icon widget - iconw.set_from_stock(gtk.STOCK_SAVE, iconSize) - button = toolbar.append_item( - 'Save', - 'Save as CSV', - 'Private', - iconw, - self.save) - return toolbar - - def save(self, *args): - filename = self.fmanager.get_filename() - if filename is None: return - lines = [] - basename, ext = os.path.splitext(filename) - if ext.lower() != '.csv': - filename += '.csv' - # todo: add csv extension - fh = file(filename, 'w', False) - for row in self.rows: - print >>fh, ','.join(row) - fh.close() - - - - - - - - -class MyToolbar(gtk.Toolbar): - """ - Compatability toolbar for pygtk2.2 and 2.4 (thanks to Steve - Chaplin in the mpl gtk backend) for basic code - - Derived must provide toolitems, eg - toolitems = [ - (button_str, tooltip_str, STOCK, callback_str), - (button_str, tooltip_str, STOCK, callback_str), - ] - - Examples: - ('CT Info', 'Load new 3d image', gtk.STOCK_NEW, 'load_image'), - ('Markers', 'Load markers from file', gtk.STOCK_OPEN, 'load_from'), - (None, None, None, None), - - None will add a separator. If the callback is 'some_callback', - derived must define - - def some_callback(self, button): - blah - - - - """ - iconSize = gtk.ICON_SIZE_SMALL_TOOLBAR - def __init__(self): - gtk.Toolbar.__init__(self) - self.set_border_width(1) - self.set_style(gtk.TOOLBAR_BOTH) - - if gtk.pygtk_version >= (2,4,0): - self._init_toolbar2_4() - else: - self._init_toolbar2_2() - - - def _init_toolbar2_2(self): - - for text, tooltip_text, stock, callback in self.toolitems: - if text == None: - self.append_space() - continue - - image = gtk.Image() - if stock.startswith("gtk"): #really stock item - image.set_from_stock(stock, self.iconSize) - else: - image.set_from_file(stock) - - w = self.append_item(text, - tooltip_text, - 'Private', - image, - getattr(self, callback) - ) - - def _init_toolbar2_4(self): - - self.tooltips = gtk.Tooltips() - - for text, tooltip_text, stock, callback in self.toolitems: - if text == None: - self.insert( gtk.SeparatorToolItem(), -1 ) - continue - image = gtk.Image() - if stock.startswith("gtk"): #really stock item - image.set_from_stock(stock, self.iconSize) - else: - image.set_from_file(stock) - tbutton = gtk.ToolButton(image, text) - self.insert(tbutton, -1) - tbutton.connect('clicked', getattr(self, callback)) - tbutton.set_tooltip(self.tooltips, tooltip_text, 'Private') - - - - self.show_all() diff --git a/pylocator/list_toolbar.py b/pylocator/list_toolbar.py deleted file mode 100644 index df57131..0000000 --- a/pylocator/list_toolbar.py +++ /dev/null @@ -1,39 +0,0 @@ -import gtk - -class ListToolbar(gtk.Toolbar): - """ - CLASS: ObserverToolbar - DESCR: - """ - - def __init__(self, configuration_list): - gtk.Toolbar.__init__(self) - - conf = configuration_list - - self.iconSize = gtk.ICON_SIZE_BUTTON - - self.set_border_width(0) - self.set_style(gtk.TOOLBAR_ICONS) - self.set_orientation(gtk.ORIENTATION_HORIZONTAL) - - self.buttons = {} - - for item in conf: - if type(item)==list: - self.__add_button( - item[0], - item[1], - item[2], - item[3] - ) - elif item=="-": - self.append_space() - self.show_all() - - def __add_button(self,stock, title, tooltip,callback): - iconw = gtk.Image() # icon widget - iconw.set_from_stock(stock, self.iconSize) - button = self.append_item(title,tooltip,'Private',iconw,callback) - self.buttons[title]=button - return button diff --git a/pylocator/main.py b/pylocator/main.py index a5bddc9..045a8c7 100644 --- a/pylocator/main.py +++ b/pylocator/main.py @@ -1,33 +1,36 @@ -#! /usr/bin/python - -import gtk -import os.path -from controller import PyLocatorController -from shared import shared - -def run_pylocator(filename=None, surface=None): - """main method to run when PyLocator is started""" - __global_preparations() - controller = PyLocatorController() - loadingSuccessful = controller.load_nifti(filename) - controller.align_surf_to_planes_view() - if loadingSuccessful: - controller.window.show() - gtk.main() - -def __global_preparations(): - user_dir = __find_userdir() - shared.set_file_selection(user_dir) - - -def __find_userdir(): - userdir = os.path.expanduser("~") - try: - from win32com.shell import shellcon, shell - userdir = shell.SHGetFolderPath(0,shellcon.CSIDL_PERSONAL,0,0) - except ImportError: - userdir = os.path.expanduser("~") - return userdir - -if __name__=="__main__": - run_pylocator() +"""Application entry points for PyLocator.""" + +from __future__ import annotations + +from .app import run_app + + +def run_pylocator(filename: str | None = None, surface: str | None = None) -> int: + """Launch the Qt-based PyLocator application. + + Parameters + ---------- + filename: + Optional path to a NIfTI file that should be loaded during start-up. + surface: + Deprecated argument kept for backward compatibility. Surfaces are not + yet supported in the Qt port and the value is ignored. + + Returns + ------- + int + The Qt application's exit code. + """ + + # ``surface`` is accepted to keep command-line compatibility with the + # legacy GTK application. The modern Qt interface does not yet expose + # surface loading, therefore the argument is intentionally unused. + _ = surface + return run_app(initial_volume=filename) + + +__all__ = ["run_pylocator"] + + +if __name__ == "__main__": # pragma: no cover - manual entry point + raise SystemExit(run_pylocator()) diff --git a/pylocator/marker_list.py b/pylocator/marker_list.py deleted file mode 100644 index 0e02a3a..0000000 --- a/pylocator/marker_list.py +++ /dev/null @@ -1,296 +0,0 @@ -from __future__ import division -import gobject -import gtk - -from dialogs import edit_coordinates, edit_label_of_marker - -from events import EventHandler -from colors import choose_one_color, tuple2gdkColor, gdkColor2tuple -from markers import Marker -from list_toolbar import ListToolbar -from shared import shared - - -class MarkerList(gtk.VBox): - paramd = {} # a dict from names to SurfParam instances - - def __init__(self): - super(MarkerList,self).__init__(self) - self.set_homogeneous(False) - EventHandler().attach(self) - self._markers = {} - self._marker_ids = {} - self.nmrk=0 - self.__ignore_sel_changed = False - - #Toolbar - toolbar = self.__create_toolbar() - self.pack_start(toolbar,False,False) - - #Empty-indicator - self.emptyIndicator = gtk.Label('No marker defined') - self.emptyIndicator.show() - self.pack_start(self.emptyIndicator, False, False) - - #create TreeView - #Fields: Index, Short filename, long FN, is_active?, opacityi - self.tree_mrk = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING) - self.nmrk = 0 - self.treev_mrk = gtk.TreeView(self.tree_mrk) - self._treev_sel = self.treev_mrk.get_selection() - self._treev_sel.connect("changed",self.treev_sel_changed) - self._treev_sel.set_mode(gtk.SELECTION_SINGLE) - renderer = gtk.CellRendererText() - renderer.set_property("xalign",1.0) - #renderer.set_xalign(0.0) - self.col1 = gtk.TreeViewColumn("#",renderer,text=0) - self.treev_mrk.append_column(self.col1) - self.col2 = gtk.TreeViewColumn("Label",renderer,text=1) - self.treev_mrk.append_column(self.col2) - self.col3 = gtk.TreeViewColumn("Position",renderer,text=2) - self.treev_mrk.append_column(self.col3) - #self.treev_mrk.show() - self.scrolledwindow = gtk.ScrolledWindow() - self.pack_start(self.scrolledwindow,True,True) - self.scrolledwindow.add(self.treev_mrk) - self.scrolledwindow.show() - - self.show_all() - - self.__update_treeview_visibility() - - def __create_toolbar(self): - conf = [ - [gtk.STOCK_ADD, - 'Add', - 'Add new marker by entering its coordinates', - self.cb_add - ], - [gtk.STOCK_REMOVE, - 'Remove', - 'Remove selected marker', - self.cb_remove - ], - "-", - [gtk.STOCK_GO_UP, - 'Move up', - 'Move selected marker up in list', - self.cb_move_up - ], - [gtk.STOCK_GO_DOWN, - 'Move down', - 'Move selected marker down in list', - self.cb_move_down - ], - "-", - [gtk.STOCK_BOLD, - 'Label', - 'Edit Label of marker', - self.cb_edit_label - ], - [gtk.STOCK_EDIT, - 'Position', - 'Edit position of marker', - self.cb_edit_position - ], - [gtk.STOCK_SELECT_COLOR, - 'Color', - 'Select color for marker', - self.cb_choose_color - ] - ] - return ListToolbar(conf) - - def update_viewer(self, event, *args): - if event=='add marker': - marker = args[0] - self.add_marker(marker) - elif event=='remove marker': - marker = args[0] - self.remove_marker(marker) - #elif event=='color marker': - # marker, color = args - # marker.set_color(color) - elif event=='label marker': - marker, label = args - #print "MarkerList:", marker.uuid, label - id_ = self._marker_ids[marker.uuid] - treeiter = self._get_iter_for_id(id_) - if treeiter: - self.tree_mrk.set(treeiter,1,str(label)) - elif event=='move marker': - marker, center = args - x,y,z = center #marker.get_center() - id_ = self._marker_ids[marker.uuid] - treeiter = self._get_iter_for_id(id_) - self.tree_mrk.set(treeiter,2,self.__format_coord_string(x,y,z)) - elif event=='select marker': - marker = args[0] - self.__set_marker_selected(marker,True) - elif event=='unselect marker': - marker = args[0] - self.__set_marker_selected(marker,False) - - def cb_add(self,*args): - parent_window = self.get_parent_window() - #print parent_window - coordinates = edit_coordinates(description="Please enter the coordinates\nfor the marker to be added") - if coordinates==None: - return - x,y,z = coordinates - marker = Marker(xyz=(x,y,z), - rgb=EventHandler().get_default_color(), - radius=shared.ratio*3) - - EventHandler().add_marker(marker) - - def cb_remove(self,*args): - marker = self.__get_selected_marker() - if marker==None: - return - EventHandler().remove_marker(marker) - - def cb_choose_color(self,*args): - marker = self.__get_selected_marker() - if marker==None: - return - old_color = marker.get_color() - new_color = choose_one_color("New color for marker",tuple2gdkColor(old_color)) - if old_color==new_color: - return - EventHandler().notify('color marker', marker, gdkColor2tuple(new_color)) - EventHandler().notify('render now') - - def cb_move_up(self, *args): - self._move_in_list(up=True) - - def cb_move_down(self, *args): - self._move_in_list(up=False) - - def cb_edit_label(self, *args): - marker = self.__get_selected_marker() - if marker==None: - return - edit_label_of_marker(marker) - - def cb_edit_position(self,*args): - marker = self.__get_selected_marker() - if marker==None: - return - x_old,y_old,z_old = marker.get_center() - #parent_window = self.get_parent_window() - #print parent_window - coordinates = edit_coordinates(x_old,y_old,z_old) - if coordinates==None: - return - x,y,z = coordinates - EventHandler().notify("move marker",marker, (x,y,z)) - self.treev_sel_changed(self._treev_sel) - - def _move_in_list(self,up=True): - if self._treev_sel.count_selected_rows == 0: - return - ( model, rows ) = self._treev_sel.get_selected_rows() - # Get new path for each selected row and swap items. */ - for path1 in rows: - # Move path2 in right direction - if up: - path2 = ( path1[0] - 1, ) - else: - path2 = ( path1[0] + 1, ) - # If path2 is negative, we're trying to move first path up. Skip - # one loop iteration. - if path2[0] < 0: - continue - # Obtain iters and swap items. If the second iter is invalid, we're - # trying to move the last item down. */ - iter1 = model.get_iter( path1 ) - try: - iter2 = model.get_iter( path2 ) - except ValueError: - continue - model.swap( iter1, iter2 ) - - def _get_iter_for_id(self,id_): - treeiter = self.tree_mrk.get_iter_first() - while treeiter: - #print self.tree_mrk.get(treeiter,0)[0], id_ - if self.tree_mrk.get(treeiter,0)[0] == id_: - break - else: - treeiter = self.tree_mrk.iter_next(treeiter) - return treeiter - - def add_marker(self,marker): - self.nmrk+=1 - self._marker_ids[marker.uuid] = self.nmrk - self._markers[self.nmrk]=marker - x,y,z = marker.get_center() - treeiter = self.tree_mrk.append(None) - self.tree_mrk.set(treeiter,0,self.nmrk,1,"",2,self.__format_coord_string(x,y,z)) - self.__update_treeview_visibility() - - def remove_marker(self,marker): - try: - id_ = self._marker_ids[marker.uuid] - treeiter = self._get_iter_for_id(id_) - if treeiter: - self.tree_mrk.remove(treeiter) - del self._markers[id_] - del self._marker_ids[marker.uuid] - except Exception, e: - print "Exception in MarkerList.remove_marker" - finally: - self.__update_treeview_visibility() - - def treev_sel_changed(self,selection): - if self.__ignore_sel_changed: - return - EventHandler().clear_selection() - try: - treeiter = selection.get_selected()[1] - if treeiter: - mrk_id = self.tree_mrk.get(treeiter,0)[0] - if mrk_id==None: - return - marker = self._markers[mrk_id] - EventHandler().select_new(marker) - except: - pass - - def __update_treeview_visibility(self): - if self.tree_mrk.get_iter_first()==None: - # tree is empty - self.emptyIndicator.show() - self.scrolledwindow.hide() - else: - self.emptyIndicator.hide() - self.scrolledwindow.show() - - def __get_selected_marker(self): - treeiter = self._treev_sel.get_selected()[1] - if not treeiter: - return - mrk_id = self.tree_mrk.get(treeiter,0)[0] - return self._markers[mrk_id] - - def __format_coord_string(self,x,y,z): - def fmt(f): - s = "%.1f"%f - return " "*(7-len(s))+s - - return "%s,%s,%s"%(fmt(x), fmt(y), fmt(z)) - - def __set_marker_selected(self, marker, selected=True): - id_ = self._marker_ids[marker.uuid] - treeiter = self._get_iter_for_id(id_) - try: - self.__ignore_sel_changed = True - if selected: - self._treev_sel.select_iter(treeiter) - else: - self._treev_sel.unselect_iter(treeiter) - except Exception: - pass - finally: - self.__ignore_sel_changed = False diff --git a/pylocator/marker_window_interactor.py b/pylocator/marker_window_interactor.py deleted file mode 100644 index 37fef62..0000000 --- a/pylocator/marker_window_interactor.py +++ /dev/null @@ -1,280 +0,0 @@ -import gtk -import vtk -from render_window import PyLocatorRenderWindow -from events import EventHandler, UndoRegistry -from shared import shared -from dialogs import edit_label_of_marker - -INTERACT_CURSOR, MOVE_CURSOR, COLOR_CURSOR, SELECT_CURSOR, DELETE_CURSOR, LABEL_CURSOR, SCREENSHOT_CURSOR = gtk.gdk.ARROW, gtk.gdk.HAND2, gtk.gdk.SPRAYCAN, gtk.gdk.TCROSS, gtk.gdk.X_CURSOR, gtk.gdk.PENCIL, gtk.gdk.ICON - -class MarkerWindowInteractor(PyLocatorRenderWindow): - """ - CLASS: MarkerWindowInteractor - DESCR: - """ - def __init__(self): - PyLocatorRenderWindow.__init__(self) - #self.camera = self.renderer.GetActiveCamera() - - - self.pressFuncs = {1 : self._Iren.LeftButtonPressEvent, - 2 : self._Iren.MiddleButtonPressEvent, - 3 : self._Iren.RightButtonPressEvent} - self.releaseFuncs = {1 : self._Iren.LeftButtonReleaseEvent, - 2 : self._Iren.MiddleButtonReleaseEvent, - 3 : self._Iren.RightButtonReleaseEvent} - - self.pressHooks = {} - self.releaseHooks = {} - - self.vtk_interact_mode = False - - def mouse1_mode_change(self, event): - """ - Give derived classes toclean up any observers, etc, before - switching to new mode - """ - - pass - - def update_viewer(self, event, *args): - PyLocatorRenderWindow.update_viewer(self, event, *args) - if event.find('mouse1')==0: - self.mouse1_mode_change(event) - if event=='mouse1 interact': - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_interact()" - self.set_mouse1_to_interact() - elif event=='vtk interact': - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_vtkinteract()" - self.set_mouse1_to_vtkinteract() - elif event=='mouse1 color': - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_color()" - self.set_mouse1_to_color() - elif event=='mouse1 delete': - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_delete()" - self.set_mouse1_to_delete() - elif event=='mouse1 label': - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_label()" - self.set_mouse1_to_label() - elif event=='mouse1 select': - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_select()" - self.set_mouse1_to_select() - elif event=='mouse1 move': - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_move()" - self.set_mouse1_to_move() - - def get_marker_at_point(self): - raise NotImplementedError - - def set_image_data(self, imageData): - pass - - def set_select_mode(self): - pass - - def set_interact_mode(self): - if shared.debug: print "set_interact_mode()!!!!" - self.vtk_interact_mode = False - - def set_vtkinteract_mode(self): - if shared.debug: print "set_vtkinteract_mode()!!!!" - - if (self.vtk_interact_mode == False): - # mcc XXX: ignore this - #foo = self.AddObserver('InteractionEvent', self.vtkinteraction_event) - #print "MarkerWindowInteractor.set_vtkinteract_mode(): AddObserver call returns ", foo - self.vtk_interact_mode = True - - def set_mouse1_to_interact(self): - - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_interact()" - - self.vtk_interact_mode = False - - # XXX why does this not work - self.set_interact_mode() - - try: del self.pressHooks[1] - except KeyError: pass - - try: del self.releaseHooks[1] - except KeyError: pass - - cursor = gtk.gdk.Cursor (INTERACT_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - def vtkinteraction_event(self, *args): - if shared.debug: print "vtkinteraction_event!!!" - self.Render() - - def set_mouse1_to_vtkinteract(self): - - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_vtkinteract()" - - self.set_vtkinteract_mode() - - def button_down(*args): - #print "button down on brain interact." - x, y = self.GetEventPosition() - picker = vtk.vtkPropPicker() - picker.PickProp(x, y, self.renderer) - actor = picker.GetActor() - # now do something with the actor !!! - #print "actor is ", actor - - def button_up(*args): - #print "button up on brain interact." - pass - - self.pressHooks[1] = button_down - self.releaseHooks[1] = button_up - - cursor = gtk.gdk.Cursor (INTERACT_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - def set_mouse1_to_move(self): - self.set_select_mode() - cursor = gtk.gdk.Cursor (MOVE_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - def set_mouse1_to_delete(self): - - def button_up(*args): - pass - - def button_down(*args): - marker = self.get_marker_at_point() - if marker is None: return - EventHandler().remove_marker(marker) - - self.pressHooks[1] = button_down - self.releaseHooks[1] = button_up - - self.set_select_mode() - cursor = gtk.gdk.Cursor (DELETE_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - def set_mouse1_to_select(self): - - def button_up(*args): - pass - - def button_down(*args): - marker = self.get_marker_at_point() - if marker is None: return - isSelected = EventHandler().is_selected(marker) - if self.interactor.GetControlKey(): - if isSelected: EventHandler().remove_selection(marker) - else: EventHandler().add_selection(marker) - else: EventHandler().select_new(marker) - - self.pressHooks[1] = button_down - self.releaseHooks[1] = button_up - - self.set_select_mode() - cursor = gtk.gdk.Cursor (SELECT_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - def set_mouse1_to_label(self): - - def button_up(*args): - pass - - def button_down(*args): - marker = self.get_marker_at_point() - if marker is None: return - edit_label_of_marker(marker) - - - self.pressHooks[1] = button_down - self.releaseHooks[1] = button_up - self.set_select_mode() - cursor = gtk.gdk.Cursor (LABEL_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - def set_mouse1_to_color(self): - - def button_up(*args): - pass - - def button_down(*args): - marker = self.get_marker_at_point() - if marker is None: return - color = EventHandler().get_default_color() - oldColor = marker.get_color() - UndoRegistry().push_command( - EventHandler().notify, 'color marker', marker, oldColor) - EventHandler().notify('color marker', marker, color) - EventHandler().notify('render now') - - self.pressHooks[1] = button_down - self.releaseHooks[1] = button_up - self.set_select_mode() - - cursor = gtk.gdk.Cursor (COLOR_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - - def OnButtonDown(self, wid, event): - """Mouse button pressed.""" - - self.lastCamera = self.get_camera_fpu() - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - if shared.debug: print "MarkerWindowInteractor.OnButtonDown(): ctrl=", ctrl,"shift=",shift,"button=",event.button - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - chr(0), 0, None) - - if shared.debug: print "MarkerWindowInteractor.OnButtonDown(): pressFuncs=", self.pressFuncs, "pressHooks=", self.pressHooks - - if event.button in self.interactButtons: - if shared.debug: print "self.vtk_interact_mode =", self.vtk_interact_mode - if (self.vtk_interact_mode == False): - self.pressFuncs[event.button]() - - try: self.pressHooks[event.button]() - except KeyError: pass - - def OnButtonUp(self, wid, event): - """Mouse button released.""" - m = self.get_pointer() - ctrl, shift = self._GetCtrlShift(event) - if shared.debug: print "MarkerWindowInteractor.OnButtonUp(): ctrl=", ctrl,"shift=",shift, "button=",event.button - self._Iren.SetEventInformationFlipY(m[0], m[1], ctrl, shift, - chr(0), 0, None) - - - if event.button in self.interactButtons: - if shared.debug: print "self.vtk_interact_mode =", self.vtk_interact_mode - if (self.vtk_interact_mode == False): - self.releaseFuncs[event.button]() - - try: self.releaseHooks[event.button]() - except KeyError: pass - - thisCamera = self.get_camera_fpu() - try: self.lastCamera - except AttributeError: pass # this - else: - if thisCamera != self.lastCamera: - UndoRegistry().push_command(self.set_camera, self.lastCamera) - - return True - - def get_plane_points(self, pw): - return pw.GetOrigin(), pw.GetPoint1(), pw.GetPoint2() - - def set_plane_points(self, pw, pnts): - o, p1, p2 = pnts - pw.SetOrigin(o) - pw.SetPoint1(p1) - pw.SetPoint2(p2) - pw.UpdatePlacement() - diff --git a/pylocator/markers.py b/pylocator/markers.py deleted file mode 100644 index 9de6baa..0000000 --- a/pylocator/markers.py +++ /dev/null @@ -1,222 +0,0 @@ -"""Define vtkActors for use in PyLocatorRenderWindow""" - -import math -import vtk -import uuid - -import numpy as n - -from shared import shared - -class Marker(vtk.vtkActor): - """ - CLASS: Marker - DESCR: Represents, e.g., an individual grid location as a vtk sphere - """ - def __init__(self, xyz, radius, rgb = None, uuid_=None): - if rgb is None: - rgb = (0, 0, 1) - - self.sphere = vtk.vtkSphereSource() - self.sphere.SetRadius(radius) - res = 20 - self.sphere.SetThetaResolution(res) - self.sphere.SetPhiResolution(res) - self.sphere.SetCenter(xyz) - mapper = vtk.vtkPolyDataMapper() - mapper.SetInput(self.sphere.GetOutput()) - mapper.ImmediateModeRenderingOn() - - self.SetMapper(mapper) - - self.GetProperty().SetColor( rgb ) - - self.set_lighting() - - self.label = '' - self.label_color = (1, 1, 0.5) - - #create ID - if not uuid_: - self.uuid = uuid.uuid1() - else: - self.uuid = uuid_ - - def set_lighting(self): - """Set the lighting of the marker actor""" - prop = self.GetProperty() - prop.SetAmbient(0.) - prop.SetDiffuse(0.) - prop.SetSpecular(1.0) - - def contains(self, xyz): - """Return true if point xyz is in the marker""" - if xyz is None: return 0 - d = math.sqrt(vtk.vtkMath.Distance2BetweenPoints( - self.sphere.GetCenter(), xyz)) - #print 'locs', xyz, self.sphere.GetCenter() - if d < self.sphere.GetRadius(): return 1 - else: return 0 - - def get_source(self): - return self.sphere - - def set_label(self, label): - self.label = label - - def get_label(self): - return self.label - - def get_label_color(self): - return self.label_color - - def set_label_color(self, color): - self.label_color = color - - def get_center(self): - return self.sphere.GetCenter() - - def set_center(self, center): - self.sphere.SetCenter(center) - - def get_size(self): - return self.sphere.GetRadius() - - def set_size(self, s): - return self.sphere.SetRadius(s) - - def set_color(self, color): - if shared.debug: print "Marker.GetProperty().SetColor(", color, ")" - self.GetProperty().SetColor( color ) - - def get_color(self): - return self.GetProperty().GetColor() - - def deep_copy(self): - m = Marker(xyz=self.sphere.GetCenter(), - radius=self.sphere.GetRadius(), - rgb=self.GetProperty().GetColor(), - uuid_=self.uuid) - m.set_label(self.get_label()) - return m - - def to_string(self): - r,g,b = self.get_color() - x,y,z = self.get_center() - radius = self.get_size() - label = self.get_label() - s = label + ',' + ','.join(map(str, (x,y,z,radius,r,g,b))) - return s - - def from_string(s): - #todo; use csv module - vals = s.replace('"', '').split(',') - label = vals[0] - x,y,z,radius,r,g,b = map(float, vals[1:]) - marker = Marker(xyz=(x,y,z), radius=radius, rgb=(r,g,b)) - marker.set_label(label) - return marker - - def get_name_num(self): - label = self.get_label() - tup = label.split() - if len(tup)!=2: return label, 0 - - name, num = tup - try: num = int(num) - except ValueError: return label, 0 - except TypeError: return label, 0 - else: return name, num - - color = property(get_color, set_color) - -class RingActor(vtk.vtkActor): - def __init__(self, marker, planeWidget, - transform=None, lineWidth=1): - - self.lineWidth=lineWidth - self.marker = marker - self.markerSource = marker.get_source() - self.planeWidget = planeWidget - self.transform = transform - - self.implicitPlane = vtk.vtkPlane() - self.ringEdges = vtk.vtkCutter() - self.ringStrips = vtk.vtkStripper() - self.ringPoly = vtk.vtkPolyData() - self.ringMapper = vtk.vtkPolyDataMapper() - - self.ringEdges.SetInput(self.markerSource.GetOutput()) - self.implicitPlane.SetNormal(self.planeWidget.GetNormal()) - self.implicitPlane.SetOrigin(self.planeWidget.GetOrigin()) - - #print 'implicit plane', self.implicitPlane - self.ringEdges.SetCutFunction(self.implicitPlane) - self.ringEdges.GenerateCutScalarsOff() - self.ringEdges.SetValue(0, 0.0) - self.ringStrips.SetInput(self.ringEdges.GetOutput()) - self.ringStrips.Update() - self.ringPoly.SetPoints(self.ringStrips.GetOutput().GetPoints()) - self.ringPoly.SetPolys(self.ringStrips.GetOutput().GetLines()) - self.ringMapper.SetInput(self.ringPoly) - self.SetMapper(self.ringMapper) - - self.lineProperty = self.GetProperty() - self.lineProperty.SetRepresentationToWireframe() - self.lineProperty.SetAmbient(1.0) - self.lineProperty.SetColor(self.marker.get_color()) - self.lineProperty.SetLineWidth(lineWidth) - - self.SetProperty(self.lineProperty) - self.VisibilityOff() - - if transform is not None: - self.filter = vtk.vtkTransformPolyDataFilter() - self.filter.SetTransform(transform) - else: - self.filter = None - self.update() - - - def update(self, *args): - # side effects update the ring poly - if not self.is_visible(): return 0 - - self.lineProperty.SetColor(self.marker.get_color()) - - if self.filter is not None: - self.filter.SetInput(self.ringPoly) - self.ringMapper.SetInput(self.filter.GetOutput()) - else: - self.ringMapper.SetInput(self.ringPoly) - self.ringMapper.Update() - self.VisibilityOn() - return 1 - - def get_marker(self): - return self.marker - - def set_line_width(self, w): - self.lineWidth = w - self.lineProperty.SetLineWidth(w) - - def get_line_width(self, w): - return self.lineWidth - - def set_selected(self, selected=False): - if selected: - self.lineProperty.SetLineWidth(self.lineWidth*5) - else: - self.lineProperty.SetLineWidth(self.lineWidth) - self.update() - - def is_visible(self): - # side effects update the ring poly; so kill me - self.implicitPlane.SetNormal(self.planeWidget.GetNormal()) - self.implicitPlane.SetOrigin(self.planeWidget.GetOrigin()) - self.ringStrips.Update() - self.ringPoly.SetPoints(self.ringStrips.GetOutput().GetPoints()) - self.ringPoly.SetPolys(self.ringStrips.GetOutput().GetLines()) - self.ringPoly.Update() - return self.ringPoly.GetNumberOfPolys() - diff --git a/pylocator/misc/markers_io.py b/pylocator/misc/markers_io.py deleted file mode 100644 index 6b42ac9..0000000 --- a/pylocator/misc/markers_io.py +++ /dev/null @@ -1,31 +0,0 @@ - -def load_markers(fh): - if type(fh)==str: - fh = open(fh,"r") - rv = [] - for line in fh.readlines(): - rv.append([]) - parts = line[:-1].split(",") - rv[-1].append(parts[0]) - for i in range(1,len(parts)): - rv[-1].append(float(parts[i])) - return rv - -def load_markers_to_dict(fh): - if type(fh)==str: - fh = open(fh,"r") - marker_list = [] - for line in fh.readlines(): - marker_list.append([]) - parts = line[:-1].split(",") - marker_list[-1].append(parts[0]) - for i in range(1,len(parts)): - marker_list[-1].append(float(parts[i])) - return dict([(marker[0],marker[1:]) for marker in marker_list]) - -if __name__=="__main__": - fn = "/media/Extern/public/Experimente/AudioStroop/kombinierte_analyse/elec_pos/443.txt" - print load_markers(fn) - fh = open(fn,"r") - print load_markers(fh) - diff --git a/pylocator/nifti_loader.py b/pylocator/nifti_loader.py new file mode 100644 index 0000000..3de8e4a --- /dev/null +++ b/pylocator/nifti_loader.py @@ -0,0 +1,76 @@ +"""Utilities for reading NIfTI volumes and converting them to VTK data.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +import nibabel as nib +import numpy as np +from vtkmodules.util import numpy_support +from vtkmodules.vtkCommonDataModel import vtkImageData + + +class NiftiLoadError(RuntimeError): + """Raised when a NIfTI volume cannot be loaded.""" + + +@dataclass(slots=True) +class NiftiVolume: + """A loaded NIfTI volume ready for rendering.""" + + image_data: vtkImageData + path: Path + shape: tuple[int, int, int] + voxel_size: tuple[float, float, float] + value_range: tuple[float, float] + + +def _to_vtk_image(array: np.ndarray, voxel_size: tuple[float, float, float]) -> vtkImageData: + image = vtkImageData() + dims = tuple(int(v) for v in array.shape) + image.SetDimensions(*dims) + image.SetSpacing(*voxel_size) + image.SetOrigin(0.0, 0.0, 0.0) + + flat = numpy_support.numpy_to_vtk( + num_array=np.ravel(array, order="F"), + deep=True, + array_type=numpy_support.get_vtk_array_type(array.dtype), + ) + image.GetPointData().SetScalars(flat) + return image + + +def load_nifti_volume(filename: str) -> NiftiVolume: + """Load *filename* into memory and return a :class:`NiftiVolume`.""" + + path = Path(filename) + if not path.exists(): + raise NiftiLoadError(f"NIfTI file does not exist: {path}") + + try: + nifti = nib.load(str(path)) + except (OSError, nib.spatialimages.ImageFileError) as exc: + raise NiftiLoadError(f"Failed to open {path}: {exc}") from exc + + data = nifti.get_fdata(dtype=np.float32) + if data.ndim != 3: + raise NiftiLoadError( + f"PyLocator currently supports 3-D volumes only, got shape {data.shape!r}" + ) + + voxel_size = tuple(float(z) for z in nifti.header.get_zooms()[:3]) + vtk_image = _to_vtk_image(data, voxel_size) + value_range = float(np.min(data)), float(np.max(data)) + + return NiftiVolume( + image_data=vtk_image, + path=path.resolve(), + shape=tuple(int(v) for v in data.shape), + voxel_size=voxel_size, + value_range=value_range, + ) + + +__all__ = ["NiftiLoadError", "NiftiVolume", "load_nifti_volume"] diff --git a/pylocator/plane_widgets_observer.py b/pylocator/plane_widgets_observer.py deleted file mode 100644 index b6491b2..0000000 --- a/pylocator/plane_widgets_observer.py +++ /dev/null @@ -1,547 +0,0 @@ -from gtk import gdk -import gtk -import vtk -import time -from markers import Marker, RingActor -from events import EventHandler, UndoRegistry - -import numpy as np - -from marker_window_interactor import MarkerWindowInteractor -from shared import shared -from rois import RoiEdgeActor - -INTERACT_CURSOR, MOVE_CURSOR, COLOR_CURSOR, SELECT_CURSOR, DELETE_CURSOR, LABEL_CURSOR, SCREENSHOT_CURSOR = gtk.gdk.ARROW, gtk.gdk.HAND2, gtk.gdk.SPRAYCAN, gtk.gdk.TCROSS, gtk.gdk.X_CURSOR, gtk.gdk.PENCIL, gtk.gdk.ICON - - -class PlaneWidgetObserver(MarkerWindowInteractor): - """ - Showing a slice view snychronised with planes widget - """ - axes_labels_color = (0.,0.82,1.) - - def __init__(self, planeWidget, owner, orientation, imageData=None): - if shared.debug: print "PlaneWidgetObserver.__init__(): orientation=",orientation - MarkerWindowInteractor.__init__(self) - self.interactButtons = (1,2,3) - self.pw = planeWidget - self.owner = owner - self.orientation = orientation - self.observer = vtk.vtkImagePlaneWidget() - - self.camera = self.renderer.GetActiveCamera() - - self.ringActors = {} - self.defaultRingLine = 1 - self.textActors = {} - self.hasData = 0 - - self.set_image_data(imageData) - self.lastTime = 0 - self.set_mouse1_to_move() - - def set_image_data(self, imageData): - if imageData is None: return - self.imageData = imageData - if not self.hasData: - if shared.debug: print "PlaneWidgetObserver(", self.orientation,").. AddObserver(self.interaction_event)" - foo = self.pw.AddObserver('InteractionEvent', self.interaction_event) - if shared.debug: print "PlaneWidgetObserver.set_image_data(): AddObserver call returns ", foo - self.connect("scroll_event", self.scroll_widget_slice) - self.hasData = 1 - - # make cursor invisible - self.observer.GetCursorProperty().SetOpacity(0.0) - - self.observer.TextureInterpolateOn() - self.observer.TextureInterpolateOn() - self.observer.SetKeyPressActivationValue( - self.pw.GetKeyPressActivationValue()) - self.observer.GetPlaneProperty().SetColor(0,0,0) - self.observer.SetResliceInterpolate( - self.pw.GetResliceInterpolate()) - self.observer.SetLookupTable(self.pw.GetLookupTable()) - self.observer.DisplayTextOn() - #self.observer.GetMarginProperty().EdgeVisibilityOff() # turn off edges?? - self.observer.SetInput(imageData) - self.observer.SetInteractor(self.interactor) - self.observer.On() - self.observer.InteractionOff() - self.update_plane() - - - #self.sliceIncrement = spacing[self.orientation] - self.sliceIncrement = 0.1 - - spacing = self.imageData.GetSpacing() - self._ratio = np.mean(np.abs(spacing)) #For marker sizes - shared.ratio = self._ratio - - - def add_axes_labels(self): - labels = shared.labels - self.axes_labels=labels - self.axes_labels_actors=[] - size = abs(self.imageData.GetSpacing()[0]) * 5 - for i,b in enumerate(self.imageData.GetBounds()): - coords = list(self.imageData.GetCenter()) - coords[i/2] = b*1.12 - idx_label = 1*i #historical reasons for using this - label = labels[idx_label] - if shared.debug: print i,b, coords, label - if self.orientation == 0: - if label in ["R","L"]: - continue - if self.orientation == 1: - if label in ["A","P"]: - continue - if self.orientation == 2: - if label in ["S","I"]: - continue - - #Orientation should be correct due to reading affine in vtkNifti - text = vtk.vtkVectorText() - text.SetText(label) - textMapper = vtk.vtkPolyDataMapper() - textMapper.SetInput(text.GetOutput()) - textActor = vtk.vtkFollower() - textActor.SetMapper(textMapper) - textActor.SetScale(size, size, size) - x,y,z = coords - textActor.SetPosition(x, y, z) - textActor.GetProperty().SetColor(*self.axes_labels_color) - textActor.SetCamera(self.camera) - self.axes_labels_actors.append(textActor) - self.renderer.AddActor(textActor) - - #Reorient camera to have head up - center = self.imageData.GetCenter() - spacing = self.imageData.GetSpacing() - bounds = np.array(self.imageData.GetBounds()) - if shared.debug: print "***center,spacing,bounds", center,spacing,bounds - pos = [center[0], center[1], center[2]] - camera_up = [0,0,0] - if self.orientation == 0: - pos[0] += max((bounds[1::2]-bounds[0::2]))*2 - camera_up[2] = 1 - elif self.orientation == 1: - pos[1] += max((bounds[1::2]-bounds[0::2]))*2 - camera_up[2] = 1 - elif self.orientation == 2: - pos[2] += max((bounds[1::2]-bounds[0::2]))*2 - camera_up[0] = -1 - if shared.debug: print camera_up - fpu = center, pos, tuple(camera_up) - if shared.debug: print "***fpu2:", fpu - self.set_camera(fpu) - self.scroll_depth(self.sliceIncrement) - - # dirty fix for oberserver not updating window/level: - # set LookupTable again. - self.observer.SetLookupTable(self.pw.GetLookupTable()) - - def mouse1_mode_change(self, event): - try: self.moveEvent - except AttributeError: pass - else: self.observer.RemoveObserver(self.moveEvent) - - try: self.startEvent - except AttributeError: pass - else: self.observer.RemoveObserver(self.startEvent) - - try: self.endEvent - except AttributeError: pass - else: self.observer.RemoveObserver(self.endEvent) - - - def set_mouse1_to_move(self): - self.markerAtPoint = None - self.pressed1 = 0 - - def move(*args): - if self.markerAtPoint is None: return - xyz = self.get_cursor_position_world() - EventHandler().notify( - 'move marker', self.markerAtPoint, xyz) - - def button_down(*args): - self.markerAtPoint = self.get_marker_at_point() - if self.markerAtPoint is not None: - self.lastPos = self.markerAtPoint.get_center() - - def button_up(*args): - if self.markerAtPoint is None: return - thisPos = self.markerAtPoint.get_center() - - def undo_move(marker): - marker.set_center(self.lastPos) - ra = self.get_actor_for_marker(marker) - ra.update() - self.Render() - - if thisPos != self.lastPos: - UndoRegistry().push_command(undo_move, self.markerAtPoint) - self.markerAtPoint = None - - - self.pressHooks[1] = button_down - self.releaseHooks[1] = button_up - - self.moveEvent = self.observer.AddObserver( - 'InteractionEvent', move) - - #self.set_select_mode() - - cursor = gtk.gdk.Cursor (MOVE_CURSOR) - if self.window is not None: - self.window.set_cursor (cursor) - - def set_select_mode(self): - return - #self.defaultRingLine = 3 - #actors = self.get_ring_actors_as_list() - #for actor in actors: - # actor.set_line_width(self.defaultRingLine) - # actor.update() - #self.Render() - - def set_interact_mode(self): - self.interactButtons = (1,2,3) - self.set_mouse1_to_move() - return - #self.defaultRingLine = 1 - #actors = self.get_ring_actors_as_list() - #for actor in actors: - # actor.set_line_width(self.defaultRingLine) - # actor.update() - #self.Render() - #self.set_mouse1_to_move() - - - def get_marker_at_point(self): - xyz = self.get_cursor_position_world() - for actor in self.get_ring_actors_as_list(): - if not actor.GetVisibility(): continue - marker = actor.get_marker() - if marker is None: return None - if marker.contains(xyz): return marker - return None - - def get_plane_points(self): - return self.pw.GetOrigin(), self.pw.GetPoint1(), self.pw.GetPoint2() - - def set_plane_points(self, pnts): - o, p1, p2 = pnts - self.pw.SetOrigin(o) - self.pw.SetPoint1(p1) - self.pw.SetPoint2(p2) - self.pw.UpdatePlacement() - self.update_plane() - - def OnButtonDown(self, wid, event): - if not self.hasData: return - self.lastPnts = self.get_plane_points() - - if event.button==1: - self.observer.InteractionOn() - - ret = MarkerWindowInteractor.OnButtonDown(self, wid, event) - return ret - - def OnButtonUp(self, wid, event): - if not hasattr(self, 'lastPnts'): return - #calling this before base class freezes the cursor at last pos - if not self.hasData: return - if event.button==1: - self.observer.InteractionOff() - MarkerWindowInteractor.OnButtonUp(self, wid, event) - - pnts = self.get_plane_points() - if pnts != self.lastPnts: - UndoRegistry().push_command(self.set_plane_points, self.lastPnts) - return True - - - def scroll_depth(self, step): - # step along the normal - p1 = np.array(self.pw.GetPoint1()) - p2 = np.array(self.pw.GetPoint2()) - - origin = self.pw.GetOrigin() - normal = self.pw.GetNormal() - newPlane = vtk.vtkPlane() - newPlane.SetNormal(normal) - newPlane.SetOrigin(origin) - newPlane.Push(step) - newOrigin = newPlane.GetOrigin() - - delta = np.array(newOrigin) - np.array(origin) - p1 += delta - p2 += delta - - self.pw.SetPoint1(p1) - self.pw.SetPoint2(p2) - self.pw.SetOrigin(newOrigin) - self.pw.UpdatePlacement() - self.update_plane() - - def scroll_axis1(self, step): - #rotate around axis 1 - axis1 = [0,0,0] - self.pw.GetVector1(axis1) - transform = vtk.vtkTransform() - - axis2 = [0,0,0] - self.pw.GetVector2(axis2) - - transform = vtk.vtkTransform() - transform.RotateWXYZ(step, - (axis1[0] + 0.5*axis2[0], - axis1[1] + 0.5*axis2[2], - axis1[2] + 0.5*axis2[2])) - o, p1, p2 = self.get_plane_points() - o = transform.TransformPoint(o) - p1 = transform.TransformPoint(p1) - p2 = transform.TransformPoint(p2) - self.set_plane_points((o, p1, p2)) - self.update_plane() - - def scroll_axis2(self, step): - axis1 = [0,0,0] - self.pw.GetVector2(axis1) - transform = vtk.vtkTransform() - - axis2 = [0,0,0] - self.pw.GetVector1(axis2) - - transform = vtk.vtkTransform() - transform.RotateWXYZ(step, - (axis1[0] + 0.5*axis2[0], - axis1[1] + 0.5*axis2[2], - axis1[2] + 0.5*axis2[2])) - o, p1, p2 = self.get_plane_points() - o = transform.TransformPoint(o) - p1 = transform.TransformPoint(p1) - p2 = transform.TransformPoint(p2) - self.set_plane_points((o, p1, p2)) - self.update_plane() - - def scroll_widget_slice(self, widget, event): - now = time.time() - elapsed = now - self.lastTime - - if elapsed < 0.001: return # swallow repeatede events - if event.direction == gdk.SCROLL_UP: step = 1 - elif event.direction == gdk.SCROLL_DOWN: step = -1 - - if self.interactor.GetShiftKey(): - self.scroll_axis1(step) - elif self.interactor.GetControlKey(): - self.scroll_axis2(step) - else: - self.scroll_depth(step*self.sliceIncrement) - - self.get_pwxyz().Render() - self.update_rings() - self.update_rois() - self.Render() - self.lastTime = time.time() - - def update_rings(self): - for actor in self.get_ring_actors_as_list(): - vis = actor.update() - textActor = self.textActors[actor.get_marker().uuid] - if vis and EventHandler().get_labels_on(): - textActor.VisibilityOn() - else: - textActor.VisibilityOff() - - def update_rois(self): - for actor in self.roi_actors.values(): - actor.update() - - def interaction_event(self, *args): - self.update_plane() - self.update_rings() - self.update_rois() - self.Render() - - def update_plane(self): - p1 = self.pw.GetPoint1() - p2 = self.pw.GetPoint2() - o = self.pw.GetOrigin() - self.observer.SetPoint1(p1) - self.observer.SetPoint2(p2) - self.observer.SetOrigin(o) - self.observer.UpdatePlacement() - self.renderer.ResetCameraClippingRange() - - def OnKeyPress(self, wid, event=None): - - if (event.keyval == gdk.keyval_from_name("i") or - event.keyval == gdk.keyval_from_name("I")): - - xyz = self.get_cursor_position_world() - if xyz is None: return - - marker = Marker(xyz=xyz, - rgb=EventHandler().get_default_color(), - radius=self._ratio*shared.marker_size) - - EventHandler().add_marker(marker) - return True - - elif (event.keyval == gdk.keyval_from_name("r") or - event.keyval == gdk.keyval_from_name("R")): - self.set_camera(self.resetCamera) - return True - - return MarkerWindowInteractor.OnKeyPress(self, wid, event) - - def update_viewer(self, event, *args): - MarkerWindowInteractor.update_viewer(self, event, *args) - if event=='add marker': - marker = args[0] - self.add_ring_actor(marker) - elif event=='remove marker': - marker = args[0] - self.remove_ring_actor(marker) - elif event=='move marker': - # ring actor will update automatically because it shares - # the sphere source - marker, pos = args - textActor = self.textActors[marker.uuid] - textActor.SetPosition(pos) - elif event=='color marker': - marker, color = args - actor = self.get_actor_for_marker(marker) - actor.update() - elif event=='label marker': - marker, label = args - self.label_ring_actor(marker, label) - elif event=='select marker': - marker = args[0] - actor = self.get_actor_for_marker(marker) - actor.set_selected(True) - elif event=='unselect marker': - marker = args[0] - actor = self.get_actor_for_marker(marker) - if actor!=None: - actor.set_selected(False) - elif event=='observers update plane': - self.update_plane() - elif event=="set axes directions": - self.add_axes_labels() - self.update_rings() - self.update_rois() - self.Render() - - def add_ring_actor(self, marker): - ringActor = RingActor(marker, self.pw, lineWidth=self.defaultRingLine) - vis = ringActor.update() - self.renderer.AddActor(ringActor) - self.ringActors[marker.uuid] = ringActor - - text = vtk.vtkVectorText() - text.SetText(marker.get_label()) - textMapper = vtk.vtkPolyDataMapper() - textMapper.SetInput(text.GetOutput()) - - textActor = vtk.vtkFollower() - textActor.SetMapper(textMapper) - size = 2*marker.get_size() - textActor.SetScale(size, size, size) - x,y,z = marker.get_center() - textActor.SetPosition(x, y, z) - textActor.SetCamera(self.camera) - textActor.GetProperty().SetColor(marker.get_label_color()) - if EventHandler().get_labels_on() and vis: - textActor.VisibilityOn() - else: - textActor.VisibilityOff() - - self.textActors[marker.uuid] = textActor - self.renderer.AddActor(textActor) - - - def remove_ring_actor(self, marker): - actor = self.get_actor_for_marker(marker) - if actor is None: - return - - self.renderer.RemoveActor(actor) - del self.ringActors[marker.uuid] - - textActor = self.textActors[marker.uuid] - self.renderer.RemoveActor(textActor) - del self.textActors[marker.uuid] - - def label_ring_actor(self, marker, label): - marker.set_label(label) - text = vtk.vtkVectorText() - text.SetText(marker.get_label()) - textMapper = vtk.vtkPolyDataMapper() - textMapper.SetInput(text.GetOutput()) - textActor = self.textActors[marker.uuid] - textActor.SetMapper(textMapper) - - def get_actor_for_marker(self, marker): - if self.ringActors.has_key(marker.uuid): - return self.ringActors[marker.uuid] - return None - - def get_ring_actors_as_list(self): - return self.ringActors.values() - - def get_cursor_position_world(self): - x, y = self.GetEventPosition() - xyz = [x, y, 0.0] - picker = vtk.vtkWorldPointPicker() - picker.Pick(xyz, self.renderer) - ppos = picker.GetPickPosition() - return ppos - #pos = self.get_cursor_position() - #if pos is None: return None - #world = self.obs_to_world(pos) - #return world - - def get_cursor_position(self): - xyzv = [0,0,0,0] - val = self.observer.GetCursorData(xyzv) - if val: return xyzv[:3] - else: return None - - def get_pwxyz(self): - return self.owner.pwxyz - - def get_pw(self): - return self.pw - - def get_orientation(self): - return self.orientation - - def obs_to_world(self, pnt): - if not self.hasData: return - spacing = self.imageData.GetSpacing() - transform = vtk.vtkTransform() - transform.Scale(spacing) - return transform.TransformPoint(pnt) - - def add_roi(self, uuid, pipe, color): - actor = RoiEdgeActor(pipe, color, self.pw) - self.renderer.AddActor(actor) - actor.color = color - self.roi_actors[uuid] = actor - - def remove_roi(self, uuid): - actor = self._get_roi_actor(uuid) - if actor: - self.renderer.RemoveActor(actor) - del self.roi_actors[uuid] - - def color_roi(self, uuid, color): - actor = self._get_roi_actor(uuid) - if actor: - actor.set_color(color) - - diff --git a/pylocator/plane_widgets_observer_toolbar.py b/pylocator/plane_widgets_observer_toolbar.py deleted file mode 100644 index 381ce2f..0000000 --- a/pylocator/plane_widgets_observer_toolbar.py +++ /dev/null @@ -1,146 +0,0 @@ -import gtk -from gtkutils import error_msg -import vtk -from events import EventHandler - -def move_pw_to_point(pw, xyz): - - n = pw.GetNormal() - o = pw.GetOrigin() - pxyz = [0,0,0] - vtk.vtkPlane.ProjectPoint(xyz, o, n, pxyz) - transform = vtk.vtkTransform() - transform.Translate(xyz[0]-pxyz[0], xyz[1]-pxyz[1], xyz[2]-pxyz[2]) - p1 = transform.TransformPoint(pw.GetPoint1()) - p2 = transform.TransformPoint(pw.GetPoint2()) - o = transform.TransformPoint(o) - - pw.SetOrigin(o) - pw.SetPoint1(p1) - pw.SetPoint2(p2) - pw.UpdatePlacement() - - - -class ObserverToolbar(gtk.Toolbar): - """ - CLASS: ObserverToolbar - DESCR: - """ - - def __init__(self, pwo): - 'pwo is a PlaneWidgetObserver' - gtk.Toolbar.__init__(self) - - self.pwo = pwo - - iconSize = gtk.ICON_SIZE_BUTTON - - self.set_border_width(5) - self.set_style(gtk.TOOLBAR_ICONS) - self.set_orientation(gtk.ORIENTATION_HORIZONTAL) - - - def ortho(button): - pw = pwo.get_pw() - o = pw.GetCenter() - xyz = pwo.obs_to_world(o) - pwxyz = pwo.get_pwxyz() - pw.SetPlaneOrientation(pwo.get_orientation()) - move_pw_to_point(pw, xyz) - pwo.update_plane() - pwo.Render() - pwxyz.Render() - - iconw = gtk.Image() # icon widget - iconw.set_from_stock(gtk.STOCK_HOME, iconSize) - self.append_item( - 'Ortho', - 'Restore orthogonality of plane widget', - 'Private', - iconw, - ortho) - - - def jumpto(button): - pwxyz = pwo.get_pwxyz() - pos = pwo.get_cursor_position() - # get the cursor if it's on, else selection - if pos is not None: - xyz = pwo.obs_to_world(pos) - else: - selected = EventHandler().get_selected() - if len(selected) !=1: return - marker = selected[0] - xyz = marker.get_center() - - pwxyz.snap_view_to_point(xyz) - - iconw = gtk.Image() # icon widget - iconw.set_from_stock(gtk.STOCK_GOTO_LAST, iconSize) - self.append_item( - 'Jump to', - 'Move the planes to the point under the cursor or\n' + - 'to the selected marker if only one marker is selected', - 'Private', - iconw, - jumpto) - - def coplanar(self): - numSelected = EventHandler().get_num_selected() - if numSelected !=3: - error_msg("You must first select exactly 3 markers", - ) - return - - # SetNormal is missing from the 4.2 python API so this is - # a long winded way of setting the pw to intersect 3 - # selected markers - m1, m2, m3 = EventHandler().get_selected() - - p1 = m1.get_center() - p2 = m2.get_center() - p3 = m3.get_center() - - pw = pwo.get_pw() - planeO = vtk.vtkPlaneSource() - planeO.SetOrigin(pw.GetOrigin()) - planeO.SetPoint1(pw.GetPoint1()) - planeO.SetPoint2(pw.GetPoint2()) - planeO.Update() - - planeN = vtk.vtkPlaneSource() - planeN.SetOrigin(p1) - planeN.SetPoint1(p2) - planeN.SetPoint2(p3) - planeN.Update() - - normal = planeN.GetNormal() - planeO.SetNormal(normal) - planeO.SetCenter( - (p1[0] + p2[0] + p3[0])/3, - (p1[1] + p2[1] + p3[1])/3, - (p1[2] + p2[2] + p3[2])/3, - ) - planeO.Update() - - pwxyz = pwo.get_pwxyz() - pw.SetOrigin(planeO.GetOrigin()) - pw.SetPoint1(planeO.GetPoint1()) - pw.SetPoint2(planeO.GetPoint2()) - pw.UpdatePlacement() - pwo.update_plane() - pwo.Render() - pwxyz.Render() - - - iconw = gtk.Image() # icon widget - iconw.set_from_stock(gtk.STOCK_EXECUTE, iconSize) - self.append_item( - 'Set plane', - 'Set the plane to be coplanar with 3 selected electrodes', - 'Private', - iconw, - coplanar) - - diff --git a/pylocator/plane_widgets_xyz.py b/pylocator/plane_widgets_xyz.py deleted file mode 100644 index 2718764..0000000 --- a/pylocator/plane_widgets_xyz.py +++ /dev/null @@ -1,333 +0,0 @@ -import vtk - -from events import EventHandler, UndoRegistry -from render_window import ThreeDimRenderWindow -from marker_window_interactor import MarkerWindowInteractor -import numpy as np - -from shared import shared - -def move_pw_to_point(pw, xyz): - - n = pw.GetNormal() - o = pw.GetOrigin() - pxyz = [0,0,0] - vtk.vtkPlane.ProjectPoint(xyz, o, n, pxyz) - transform = vtk.vtkTransform() - transform.Translate(xyz[0]-pxyz[0], xyz[1]-pxyz[1], xyz[2]-pxyz[2]) - p1 = transform.TransformPoint(pw.GetPoint1()) - p2 = transform.TransformPoint(pw.GetPoint2()) - o = transform.TransformPoint(o) - - pw.SetOrigin(o) - pw.SetPoint1(p1) - pw.SetPoint2(p2) - pw.UpdatePlacement() - - -class PlaneWidgetsXYZ(ThreeDimRenderWindow, MarkerWindowInteractor): - """ - CLASS: PlaneWidgetsXYZ - - DESCR: Upper left frame of window. Contains 3 rotatable image plane - widgets, and possibly a .vtk mesh. - """ - axes_labels_color = (0.,0.82,1.) - - def __init__(self, imageData=None): - MarkerWindowInteractor.__init__(self) - ThreeDimRenderWindow.__init__(self) - - if shared.debug: print "PlaneWidgetsXYZ.__init__()" - - self.vtksurface = None - - self.interactButtons = (1,2,3) - self.sharedPicker = vtk.vtkCellPicker() - #self.sharedPicker.SetTolerance(0.005) - self.SetPicker(self.sharedPicker) - - self.pwX = vtk.vtkImagePlaneWidget() - self.pwY = vtk.vtkImagePlaneWidget() - self.pwZ = vtk.vtkImagePlaneWidget() - - - self.axes_labels = [] - - self.set_image_data(imageData) - self.Render() - - self.vtk_translation = np.zeros(3, 'd') - self.vtk_rotation = np.zeros(3, 'd') - - - def translate_vtk(self, axis, value): - if (axis == 'x'): ax = 0 - elif (axis == 'y'): ax = 1 - elif (axis == 'z'): ax = 2 - self.vtk_translation[ax]=value - self.scaleTransform.Identity() - self.scaleTransform.RotateX(self.vtk_rotation[0]) - self.scaleTransform.RotateY(self.vtk_rotation[1]) - self.scaleTransform.RotateZ(self.vtk_rotation[2]) - self.scaleTransform.Translate(self.vtk_translation[0],self.vtk_translation[1],self.vtk_translation[2]) - self.Render() - - def rotate_vtk(self, axis, value): - if (axis == 'x'): ax = 0 - elif (axis == 'y'): ax = 1 - elif (axis == 'z'): ax = 2 - self.vtk_rotation[ax]=value - self.scaleTransform.Identity() - self.scaleTransform.RotateX(self.vtk_rotation[0]) - self.scaleTransform.RotateY(self.vtk_rotation[1]) - self.scaleTransform.RotateZ(self.vtk_rotation[2]) - self.scaleTransform.Translate(self.vtk_translation[0],self.vtk_translation[1],self.vtk_translation[2]) - self.Render() - - def set_image_data(self, imageData): - if shared.debug: print "PlaneWidgetsXYZ.set_image_data()!!" - if imageData is None: return - self.imageData = imageData - extent = self.imageData.GetBounds()#Extent() - if shared.debug: print "***Extent:", extent - frac = 0.3 - - self._plane_widget_boilerplate( - self.pwX, key='x', color=(1,0,0), - index=frac*(extent[1]-extent[0]), - orientation=0) - - self._plane_widget_boilerplate( - self.pwY, key='y', color=(1,1,0), - index=frac*(extent[3]-extent[2]), - orientation=1) - self.pwY.SetLookupTable(self.pwX.GetLookupTable()) - - self._plane_widget_boilerplate( - self.pwZ, key='z', color=(0,0,1), - index=frac*(extent[5]-extent[4]), - orientation=2) - self.pwZ.SetLookupTable(self.pwX.GetLookupTable()) - - self.pwX.SetResliceInterpolateToCubic() - self.pwY.SetResliceInterpolateToCubic() - self.pwZ.SetResliceInterpolateToCubic() - for pw in [self.pwX,self.pwY,self.pwZ]: - move_pw_to_point(pw,self.imageData.GetCenter()) - #self.pwZ.SetResliceInterpolateToNearestNeighbour() - self.camera = self.renderer.GetActiveCamera() - - def add_axes_labels(self): - #if shared.debug: print "***Adding axes labels" - #if shared.debug: print labels - labels = shared.labels - #labels = list(np.array(labels)[[4,5,2,3,0,1]]) - self.axes_labels=labels - self.axes_labels_actors=[] - size = abs(self.imageData.GetSpacing()[0]) * 5 - #if shared.debug: print "***size", size - for i,b in enumerate(self.imageData.GetBounds()): - coords = list(self.imageData.GetCenter()) - coords[i/2] = b*1.1 - #Correction for negative spacings - idx_label = 1*i - label = labels[idx_label] - if shared.debug: print i,b, coords, label - #Orientation should be correct due to reading affine in vtkNifti - text = vtk.vtkVectorText() - text.SetText(label) - textMapper = vtk.vtkPolyDataMapper() - textMapper.SetInput(text.GetOutput()) - textActor = vtk.vtkFollower() - textActor.SetMapper(textMapper) - textActor.SetScale(size, size, size) - x,y,z = coords - textActor.SetPosition(x, y, z) - textActor.GetProperty().SetColor(*self.axes_labels_color) - textActor.SetCamera(self.camera) - self.axes_labels_actors.append(textActor) - self.renderer.AddActor(textActor) - - #Reorient camera to have head up - center = self.imageData.GetCenter() - spacing = self.imageData.GetSpacing() - bounds = np.array(self.imageData.GetBounds()) - if shared.debug: print "***center,spacing,bounds", center,spacing,bounds - #idx_left = labels.index("L") - pos = [center[0], center[1], center[2]] - pos[0] += max((bounds[1::2]-bounds[0::2]))*2 - #idx_sup = labels.index("S") - camera_up = [0,0,0] - camera_up[2] = 1 - if shared.debug: print camera_up - fpu = center, pos, tuple(camera_up) - if shared.debug: print "***fpu2:", fpu - self.set_camera(fpu) - - def get_marker_at_point(self): - - x, y = self.GetEventPosition() - picker = vtk.vtkPropPicker() - picker.PickProp(x, y, self.renderer, EventHandler().get_markers()) - actor = picker.GetActor() - return actor - - def update_viewer(self, event, *args): - MarkerWindowInteractor.update_viewer(self, event, *args) - if event=='color marker': - marker, color = args - marker.set_color(color) - elif event=='label marker': - marker, label = args - marker.set_label(label) - - if shared.debug: print "Create VTK-Text", marker.get_label() - text = vtk.vtkVectorText() - text.SetText(marker.get_label()) - textMapper = vtk.vtkPolyDataMapper() - textMapper.SetInput(text.GetOutput()) - textActor = self.textActors[marker] - textActor.SetMapper(textMapper) - - elif event=='move marker': - marker, center = args - marker.set_center(center) - #update the select boxes and text actor - textActor = self.textActors[marker] - size = marker.get_size() - textActor.SetScale(size, size, size) - x,y,z = marker.get_center() - textActor.SetPosition(x+size, y+size, z+size) - - if self.boxes.has_key(marker): - selectActor = self.boxes[marker] - boxSource = vtk.vtkCubeSource() - boxSource.SetBounds(marker.GetBounds()) - mapper = vtk.vtkPolyDataMapper() - mapper.SetInput(boxSource.GetOutput()) - selectActor.SetMapper(mapper) - - elif event=='labels on': - actors = self.textActors.values() - for actor in actors: - actor.VisibilityOn() - elif event=='labels off': - actors = self.textActors.values() - for actor in actors: - actor.VisibilityOff() - #elif event=='select marker': - # marker = args[0] - # actor = create_box_actor_around_marker(marker) - # if shared.debug: print "PlaneWidgetsXYZ.update_viewer(): self.renderer.AddActor(actor)" - # self.renderer.AddActor(actor) - # self.boxes[marker] = actor - #elif event=='unselect marker': - # marker = args[0] - # actor = self.boxes[marker] - # print "pwxyz: u m", repr(marker), repr(actor) - # self.renderer.RemoveActor(actor) - # del self.boxes[marker] - elif event=="set axes directions": - self.add_axes_labels() - EventHandler().notify('observers update plane') - - self.Render() - - - - def _plane_widget_boilerplate(self, pw, key, color, index, orientation): - - if shared.debug: print "PlaneWidgetsXYZ._plane_widget_boilerplate(", index , orientation,")" - pw.TextureInterpolateOn() - #pw.SetResliceInterpolateToCubic() - pw.SetKeyPressActivationValue(key) - if shared.debug: print "pw " , orientation, ".SetPicker(self.sharedPicker)" - pw.SetPicker(self.sharedPicker) - pw.GetPlaneProperty().SetColor(color) - pw.DisplayTextOn() - pw.SetInput(self.imageData) - pw.SetPlaneOrientation(orientation) - pw.SetSliceIndex(int(index)) - if shared.debug: print "pw " , orientation, ".SetInteractor(self.interactor)" - pw.SetInteractor(self.interactor) - pw.On() - pw.UpdatePlacement() - - def get_plane_widget_x(self): - return self.pwX - - def get_plane_widget_y(self): - return self.pwY - - def get_plane_widget_z(self): - return self.pwZ - - def get_plane_widgets_xyz(self): - return (self.get_plane_widget_x(), - self.get_plane_widget_y(), - self.get_plane_widget_z()) - - def snap_view_to_point(self, xyz): - - # project the point onto the plane, find the distance between - # xyz and the projected point, then move the plane along it's - # normal that distance - - #todo: undo - move_pw_to_point(self.pwX, xyz) - move_pw_to_point(self.pwY, xyz) - move_pw_to_point(self.pwZ, xyz) - self.Render() - EventHandler().notify('observers update plane') - - - def set_plane_points_xyz(self, pxyz): - px, py, pz = pxyz - self.set_plane_points(self.pwX, px) - self.set_plane_points(self.pwY, py) - self.set_plane_points(self.pwZ, pz) - self.Render() - EventHandler().notify('observers update plane') - - def set_select_mode(self): - self.interactButtons = (2,3) - - def set_interact_mode(self): - self.interactButtons = (1,2,3) - - def OnButtonDown(self, wid, event): - """Mouse button pressed.""" - - if shared.debug: print "PlaneWidgetsXYZ.OnButtonDown(): event=", event - - self.lastPntsXYZ = ( self.get_plane_points(self.pwX), - self.get_plane_points(self.pwY), - self.get_plane_points(self.pwZ)) - if shared.debug: print "PlaneWidgetsXYZ.OnButtonDown(): self.lastPntsXYZ=", self.lastPntsXYZ - - - MarkerWindowInteractor.OnButtonDown(self, wid, event) - if shared.debug: print self.axes_labels - return True - - def OnButtonUp(self, wid, event): - """Mouse button released.""" - - if shared.debug: print "PlaneWidgetsXYZ.OnButtonUp(): event=", event - - if not hasattr(self, 'lastPntsXYZ'): return - MarkerWindowInteractor.OnButtonUp(self, wid, event) - pntsXYZ = ( self.get_plane_points(self.pwX), - self.get_plane_points(self.pwY), - self.get_plane_points(self.pwZ)) - - - if pntsXYZ != self.lastPntsXYZ: - UndoRegistry().push_command( - self.set_plane_points_xyz, self.lastPntsXYZ) - - - return True - - diff --git a/pylocator/qt/__init__.py b/pylocator/qt/__init__.py new file mode 100644 index 0000000..7b42adb --- /dev/null +++ b/pylocator/qt/__init__.py @@ -0,0 +1,8 @@ +"""Qt user interface components for PyLocator.""" + +from __future__ import annotations + +from .main_window import MainWindow +from .views import SliceView, VolumeView + +__all__ = ["MainWindow", "SliceView", "VolumeView"] diff --git a/pylocator/qt/main_window.py b/pylocator/qt/main_window.py new file mode 100644 index 0000000..1035a6a --- /dev/null +++ b/pylocator/qt/main_window.py @@ -0,0 +1,164 @@ +"""Main Qt window for the PyLocator application.""" + +from __future__ import annotations + +from collections import OrderedDict + +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtWidgets import ( + QDockWidget, + QFormLayout, + QGridLayout, + QLabel, + QMainWindow, + QSlider, + QTextEdit, + QToolBar, + QWidget, +) + +from ..nifti_loader import NiftiVolume +from .views import SliceView, VolumeView + + +class MainWindow(QMainWindow): + """Top-level PyLocator window.""" + + request_open_file = Signal() + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.setWindowTitle("PyLocator") + self.resize(1400, 900) + + self._current_volume: NiftiVolume | None = None + + self._create_actions() + self._create_toolbar() + self._create_views() + self._create_info_dock() + self._create_slice_controls() + self.statusBar().showMessage("Ready") + + # ------------------------------------------------------------------ + # Qt UI helpers + # ------------------------------------------------------------------ + def _create_actions(self) -> None: + self.open_action = QAction("&Open…", self) + self.open_action.setShortcut(QKeySequence.Open) + self.open_action.triggered.connect(self.request_open_file) + + self.close_action = QAction("E&xit", self) + self.close_action.setShortcut(QKeySequence.Quit) + self.close_action.triggered.connect(self.close) + + def _create_toolbar(self) -> None: + file_menu = self.menuBar().addMenu("&File") + file_menu.addAction(self.open_action) + file_menu.addSeparator() + file_menu.addAction(self.close_action) + + toolbar = QToolBar("File", self) + toolbar.setObjectName("fileToolbar") + toolbar.addAction(self.open_action) + self.addToolBar(toolbar) + + def _create_views(self) -> None: + container = QWidget(self) + layout = QGridLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + + self._slice_views: OrderedDict[str, SliceView] = OrderedDict() + for column, orientation in enumerate(("axial", "coronal")): + view = SliceView(orientation, container) + self._slice_views[orientation] = view + layout.addWidget(view.widget, 0, column) + + sagittal_view = SliceView("sagittal", container) + self._slice_views["sagittal"] = sagittal_view + layout.addWidget(sagittal_view.widget, 1, 0) + + self._volume_view = VolumeView(container) + layout.addWidget(self._volume_view.widget, 1, 1) + + container.setLayout(layout) + self.setCentralWidget(container) + + def _create_info_dock(self) -> None: + self._info_panel = QTextEdit(self) + self._info_panel.setObjectName("infoPanel") + self._info_panel.setReadOnly(True) + + dock = QDockWidget("Volume information", self) + dock.setObjectName("volumeInfoDock") + dock.setWidget(self._info_panel) + self.addDockWidget(Qt.RightDockWidgetArea, dock) + + def _create_slice_controls(self) -> None: + widget = QWidget(self) + layout = QFormLayout(widget) + layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) + + self._slice_sliders: dict[str, QSlider] = {} + for orientation in self._slice_views: + label = QLabel(orientation.capitalize(), widget) + slider = QSlider(Qt.Horizontal, widget) + slider.setObjectName(f"sliceSlider_{orientation}") + slider.setMinimum(0) + slider.setMaximum(0) + slider.setEnabled(False) + slider.valueChanged.connect( + lambda value, orient=orientation: self._slice_views[orient].set_slice(value) + ) + self._slice_sliders[orientation] = slider + layout.addRow(label, slider) + + dock = QDockWidget("Slice controls", self) + dock.setObjectName("sliceControlsDock") + dock.setWidget(widget) + self.addDockWidget(Qt.RightDockWidgetArea, dock) + dock.setFloating(False) + + # ------------------------------------------------------------------ + # Rendering helpers + # ------------------------------------------------------------------ + def display_volume(self, volume: NiftiVolume) -> None: + """Render *volume* inside the VTK viewports.""" + + self._current_volume = volume + self._volume_view.set_volume(volume) + + for orientation, view in self._slice_views.items(): + geometry = view.set_volume(volume) + slider = self._slice_sliders[orientation] + slider.blockSignals(True) + slider.setEnabled(True) + slider.setMinimum(geometry.minimum) + slider.setMaximum(geometry.maximum) + slider.setValue(geometry.current) + slider.blockSignals(False) + + self._update_info_panel(volume) + self.statusBar().showMessage(f"Loaded {volume.path.name}") + + # ------------------------------------------------------------------ + # Info panel helpers + # ------------------------------------------------------------------ + def _update_info_panel(self, volume: NiftiVolume) -> None: + location = volume.path + shape = " × ".join(str(v) for v in volume.shape) + vox = ", ".join(f"{v:.3g}" for v in volume.voxel_size) + vmin, vmax = volume.value_range + + info = ( + f"Path: {location}\n" + f"Dimensions: {shape}\n" + f"Voxel size: {vox} mm\n" + f"Value range: {vmin:.3g} – {vmax:.3g}" + ) + self._info_panel.setPlainText(info) + + +__all__ = ["MainWindow"] diff --git a/pylocator/qt/views.py b/pylocator/qt/views.py new file mode 100644 index 0000000..7f4699e --- /dev/null +++ b/pylocator/qt/views.py @@ -0,0 +1,195 @@ +"""Reusable VTK-backed view widgets for the Qt UI.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from PySide6.QtWidgets import QWidget +from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor +from vtkmodules.vtkCommonDataModel import vtkPiecewiseFunction +from vtkmodules.vtkInteractionStyle import ( + vtkInteractorStyleImage, + vtkInteractorStyleTrackballCamera, +) +from vtkmodules.vtkRenderingCore import ( + vtkColorTransferFunction, + vtkImageProperty, + vtkImageSlice, + vtkImageSliceMapper, + vtkRenderer, + vtkVolume, + vtkVolumeProperty, +) +try: + from vtkmodules.vtkRenderingVolumeOpenGL2 import vtkSmartVolumeMapper +except ImportError: # pragma: no cover + from vtkmodules.vtkRenderingVolume import vtkSmartVolumeMapper + +from ..nifti_loader import NiftiVolume + + +@dataclass(slots=True) +class SliceGeometry: + """Metadata describing how a slice view maps to the volume indices.""" + + orientation: str + minimum: int + maximum: int + current: int + + +class _BaseVTKView: + """Common helpers for views backed by :class:`QVTKRenderWindowInteractor`.""" + + def __init__( + self, + parent: QWidget | None = None, + *, + interactor_style: type | None = None, + ) -> None: + self.widget = QVTKRenderWindowInteractor(parent) + self.widget.Initialize() + self.renderer = vtkRenderer() + render_window = self.widget.GetRenderWindow() + render_window.AddRenderer(self.renderer) + self._interactor = render_window.GetInteractor() + self._interactor.Initialize() + if interactor_style is not None: + self._interactor.SetInteractorStyle(interactor_style()) + # ``Start`` wires up the Qt event pump to the interactor so user + # interactions (mouse rotation, zoom, etc.) are recognised. + self.widget.Start() + + def render(self) -> None: + """Trigger a redraw of the underlying VTK window.""" + + self.widget.GetRenderWindow().Render() + + +class VolumeView(_BaseVTKView): + """3-D volume rendering viewport.""" + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent, interactor_style=vtkInteractorStyleTrackballCamera) + self.renderer.SetBackground(0.1, 0.1, 0.1) + + def set_volume(self, volume: NiftiVolume) -> None: + """Render *volume* using VTK's smart volume mapper.""" + + self.renderer.RemoveAllViewProps() + + mapper = vtkSmartVolumeMapper() + mapper.SetInputData(volume.image_data) + + min_val, max_val = volume.value_range + if max_val - min_val < 1e-5: + max_val = min_val + 1.0 + + color_tf = vtkColorTransferFunction() + color_tf.AddRGBPoint(min_val, 0.0, 0.0, 0.0) + color_tf.AddRGBPoint(max_val, 1.0, 1.0, 1.0) + + opacity_tf = vtkPiecewiseFunction() + opacity_tf.AddPoint(min_val, 0.0) + opacity_tf.AddPoint(max_val, 1.0) + + properties = vtkVolumeProperty() + properties.SetColor(color_tf) + properties.SetScalarOpacity(opacity_tf) + properties.SetInterpolationTypeToLinear() + + actor = vtkVolume() + actor.SetMapper(mapper) + actor.SetProperty(properties) + + self.renderer.AddVolume(actor) + self.renderer.ResetCamera() + self.render() + + +class SliceView(_BaseVTKView): + """Orthogonal 2-D slice view into the volume.""" + + def __init__(self, orientation: str, parent: QWidget | None = None) -> None: + super().__init__(parent, interactor_style=vtkInteractorStyleImage) + self.orientation = orientation + self._volume: NiftiVolume | None = None + self._mapper = vtkImageSliceMapper() + self._actor = vtkImageSlice() + self._actor.SetMapper(self._mapper) + self.renderer.AddViewProp(self._actor) + self.renderer.GetActiveCamera().ParallelProjectionOn() + self.renderer.SetBackground(0.0, 0.0, 0.0) + self._geometry: SliceGeometry | None = None + + if orientation == "axial": + self._mapper.SetOrientationToZ() + self.renderer.GetActiveCamera().SetViewUp(0.0, 1.0, 0.0) + self.renderer.GetActiveCamera().SetPosition(0.0, 0.0, 1.0) + elif orientation == "coronal": + self._mapper.SetOrientationToY() + self.renderer.GetActiveCamera().SetViewUp(0.0, 0.0, 1.0) + self.renderer.GetActiveCamera().SetPosition(0.0, -1.0, 0.0) + elif orientation == "sagittal": + self._mapper.SetOrientationToX() + self.renderer.GetActiveCamera().SetViewUp(0.0, 0.0, 1.0) + self.renderer.GetActiveCamera().SetPosition(1.0, 0.0, 0.0) + else: # pragma: no cover - guarded by callers + raise ValueError(f"Unsupported orientation: {orientation}") + + @property + def geometry(self) -> SliceGeometry | None: + """Return the current slice geometry metadata.""" + + return self._geometry + + def set_volume(self, volume: NiftiVolume) -> SliceGeometry: + """Bind *volume* to the slice view and return slice metadata.""" + + self._volume = volume + image = volume.image_data + self._mapper.SetInputData(image) + + extent = image.GetExtent() + if self.orientation == "axial": + minimum, maximum = extent[4], extent[5] + elif self.orientation == "coronal": + minimum, maximum = extent[2], extent[3] + else: # sagittal + minimum, maximum = extent[0], extent[1] + + current = (minimum + maximum) // 2 + geometry = SliceGeometry(self.orientation, minimum, maximum, current) + self._geometry = geometry + self._mapper.SetSliceNumber(current) + + prop: vtkImageProperty = self._actor.GetProperty() + min_val, max_val = volume.value_range + if max_val - min_val < 1e-5: + max_val = min_val + 1.0 + prop.SetColorLevel((max_val + min_val) / 2.0) + prop.SetColorWindow(max_val - min_val) + prop.SetInterpolationTypeToLinear() + + self.renderer.ResetCamera() + self.render() + return geometry + + def set_slice(self, index: int) -> None: + """Update the displayed slice to *index*.""" + + if self._volume is None: + return + + if self._geometry is None: + raise RuntimeError("Slice geometry is not initialised") + + clamped = max(self._geometry.minimum, min(self._geometry.maximum, index)) + self._mapper.SetSliceNumber(clamped) + self._geometry = SliceGeometry( + self._geometry.orientation, self._geometry.minimum, self._geometry.maximum, clamped + ) + self.render() + + +__all__ = ["SliceView", "SliceGeometry", "VolumeView"] diff --git a/pylocator/render_window.py b/pylocator/render_window.py deleted file mode 100644 index b9ba9f7..0000000 --- a/pylocator/render_window.py +++ /dev/null @@ -1,282 +0,0 @@ -import gtk -import vtk -from GtkGLExtVTKRenderWindowInteractor import GtkGLExtVTKRenderWindowInteractor -from events import EventHandler -from gtkutils import error_msg -from vtkutils import create_box_actor_around_marker - -from shared import shared - - -INTERACT_CURSOR, MOVE_CURSOR, COLOR_CURSOR, SELECT_CURSOR, DELETE_CURSOR, LABEL_CURSOR, SCREENSHOT_CURSOR = gtk.gdk.ARROW, gtk.gdk.HAND2, gtk.gdk.SPRAYCAN, gtk.gdk.TCROSS, gtk.gdk.X_CURSOR, gtk.gdk.PENCIL, gtk.gdk.ICON - -class PyLocatorRenderWindow(GtkGLExtVTKRenderWindowInteractor): - background = (0.,0.,0.) - - def __init__(self,*args): - GtkGLExtVTKRenderWindowInteractor.__init__(self,*args) - self.screenshot_button_label = "_render window_" - self.roi_actors = {} - - EventHandler().attach(self) - self.interactButtons = (1,2,3) - self.renderOn = 1 - self.Initialize() - self.Start() - - self.renderer = vtk.vtkRenderer() - self.renderer.SetBackground(self.background) - self.renWin = self.GetRenderWindow() - self.renWin.AddRenderer(self.renderer) - self.interactor = self.renWin.GetInteractor() - - self.camera_default_fpu = None - self.camera = self.renderer.GetActiveCamera() - - def Render(self): - if self.renderOn: - GtkGLExtVTKRenderWindowInteractor.Render(self) - - def get_camera_fpu(self): - camera = self.renderer.GetActiveCamera() - return (camera.GetFocalPoint(), - camera.GetPosition(), - camera.GetViewUp()) - - def set_camera(self, fpu): - camera = self.renderer.GetActiveCamera() - focal, position, up = fpu - camera.SetFocalPoint(focal) - camera.SetPosition(position) - camera.SetViewUp(up) - self.renderer.ResetCameraClippingRange() - self.Render() - - def store_camera_default(self): - self.camera_default_fpu = self.get_camera_fpu() - - def reset_camera_to_default(self): - fpu = self.camera_default_fpu - if fpu != None: - self.set_camera(fpu) - - def take_screenshot(self, fn_pattern, magnification=1): - #print "Start Screenshot" - if not fn_pattern: - error_msg("Cannot take screenshot: No filename pattern given.") - return False - - fn = fn_pattern%shared.screenshot_cnt - mag = int(round(magnification)) - - shared.screenshot_cnt+=1 - - w2if = vtk.vtkWindowToImageFilter() - w2if.SetInput(self.renWin) - w2if.SetMagnification(mag) #shared.screenshot_magnification) - w2if.Update() - - writer = vtk.vtkPNGWriter() - writer.SetFileName(fn) - writer.SetInput(w2if.GetOutput()) - writer.Write() - #print "Ende Screenshot" - self.Render() - return - - def set_screenshot_props(self, label): - self.screenshot_button_label = label - - def set_image_data(self, image_data): - pass - - def add_marker(self, marker): - pass - - def remove_marker(self, marker): - pass - - def set_labels_visibility(self, visible=True): - pass - - def set_marker_selection(self, marker, select=True): - pass - - def add_surface(self, uuid, pipe, color): - pass - - def remove_surface(self, uuid): - pass - - def color_surface(self, uuid, color): - pass - - def change_surface_opacity(self, uuid, opactiy): - pass - - def add_roi(self, uuid, pipe, color): - pass - - def remove_roi(self, uuid): - pass - - def color_roi(self, uuid, color): - pass - - def change_roi_opacity(self, uuid, opactiy): - pass - - def _get_roi_actor(self, uuid): - if not self.roi_actors.has_key(uuid): - return - return self.roi_actors[uuid] - - def update_viewer(self, event, *args): - if event=='render off': - self.renderOn = 0 - elif event=='render on': - self.renderOn = 1 - self.Render() - elif event=='render now': - self.Render() - elif event=='set image data': - imageData = args[0] - self.set_image_data(imageData) - self.Render() - elif event=='add marker': - marker = args[0] - self.add_marker(marker) - elif event=='remove marker': - marker = args[0] - self.remove_marker(marker) - elif event=='labels on': - self.set_labels_visibility(True) - elif event=='labels off': - self.set_labels_visibility(False) - elif event=='select marker': - marker = args[0] - self.set_marker_selection(marker, True) - elif event=='unselect marker': - marker = args[0] - self.set_marker_selection(marker, False) - elif event=='add surface': - uuid, pipe, color = args - self.add_surface(uuid, pipe, color) - elif event=='remove surface': - uuid = args[0] - self.remove_surface(uuid) - elif event=='color surface': - uuid, color = args - self.color_surface(uuid, color) - elif event=='add roi': - uuid, pipe, color = args - self.add_roi(uuid, pipe, color) - elif event=='remove roi': - uuid = args[0] - self.remove_roi(uuid) - elif event=='color roi': - uuid, color = args - self.color_roi(uuid, color) - elif event=='change surface opacity': - uuid, opacity = args - self.change_surface_opacity(uuid, opacity) - elif event=='change roi opacity': - uuid, opacity = args - self.change_roi_opacity(uuid, opacity) - self.Render() - -class ThreeDimRenderWindow(object): - textActors = {} - - def __init__(self): - self.boxes = {} - - def add_marker(self, marker): - self.renderer.AddActor(marker) - - text = vtk.vtkVectorText() - text.SetText(marker.get_label()) - textMapper = vtk.vtkPolyDataMapper() - textMapper.SetInput(text.GetOutput()) - - textActor = vtk.vtkFollower() - textActor.SetMapper(textMapper) - size = marker.get_size() - textActor.SetScale(size, size, size) - x,y,z = marker.get_center() - textActor.SetPosition(x+size, y+size, z+size) - textActor.SetCamera(self.camera) - textActor.GetProperty().SetColor(marker.get_label_color()) - if EventHandler().get_labels_on(): - if shared.debug: print "VisibilityOn" - textActor.VisibilityOn() - else: - if shared.debug: print "VisibilityOff" - textActor.VisibilityOff() - self.textActors[marker] = textActor - self.renderer.AddActor(textActor) - - def remove_marker(self, marker): - self.renderer.RemoveActor(marker) - try: - self.renderer.RemoveActor(self.textActors[marker]) - del self.textActors[marker] - except KeyError: - pass - - def set_marker_selection(self, marker, select=True): - if select: - actor = create_box_actor_around_marker(marker) - if shared.debug: print "PlaneWidgetsXYZ.update_viewer(): self.renderer.AddActor(actor)" - self.renderer.AddActor(actor) - self.boxes[marker] = actor - else: - actor = self.boxes[marker] - self.renderer.RemoveActor(actor) - - def add_roi(self, uuid, pipe, color): - isoActor = self._create_actor(pipe) - isoActor.GetProperty().SetColor(color) - self._set_roi_lighting(isoActor) - self.roi_actors[uuid] = isoActor - - def remove_roi(self, uuid): - actor = self._get_roi_actor(uuid) - if actor: - self.renderer.RemoveActor(actor) - del self.roi_actors[uuid] - - def color_roi(self, uuid, color): - actor = self._get_roi_actor(uuid) - if actor: - p = actor.GetProperty() - p.SetColor(color) - - def change_roi_opacity(self, uuid, opacity): - actor = self._get_roi_actor(uuid) - if actor: - actor.GetProperty().SetOpacity(opacity) - return self.roi_actors[uuid] - - def _create_actor(self,pipe): - isoMapper = vtk.vtkPolyDataMapper() - isoMapper.SetInput(pipe.GetOutput()) - isoMapper.ScalarVisibilityOff() - - isoActor = vtk.vtkActor() - isoActor.SetMapper(isoMapper) - self.renderer.AddActor(isoActor) - return isoActor - - def _set_roi_lighting(self, isoActor): - surf_prop = isoActor.GetProperty() - surf_prop.SetAmbient(.2) - surf_prop.SetDiffuse(.3) - surf_prop.SetSpecular(.5) - - def _set_surface_lighting(self, isoActor): - surf_prop = isoActor.GetProperty() - surf_prop.SetAmbient(.2) - surf_prop.SetDiffuse(.3) - surf_prop.SetSpecular(.5) - diff --git a/pylocator/resources.py b/pylocator/resources.py deleted file mode 100644 index def9ddf..0000000 --- a/pylocator/resources.py +++ /dev/null @@ -1,21 +0,0 @@ -import os.path - - -#GTK Builder files -main_window = os.path.join(os.path.split(__file__)[0],"resources/mainWindow.glade") -edit_label_dialog = os.path.join(os.path.split(__file__)[0],"resources/editLabel.glade") -edit_coordinates_dialog = os.path.join(os.path.split(__file__)[0],"resources/editCoordinates.glade") -edit_settings_dialog = os.path.join(os.path.split(__file__)[0],"resources/editSettings.glade") -about_dialog = os.path.join(os.path.split(__file__)[0],"resources/about.glade") - -#image files -camera_fn = os.path.join(os.path.split(__file__)[0],"resources/camera48.png") -camera_small_fn = os.path.join(os.path.split(__file__)[0],"resources/camera24.png") - -if __name__=="__main__": - import gtk - builder = gtk.Builder() - builder.add_from_file(main_window) - win = builder.get_object("pylocatorMainWindow") - win.show_all() - gtk.main() diff --git a/pylocator/resources/about.glade b/pylocator/resources/about.glade deleted file mode 100644 index e68c9d4..0000000 --- a/pylocator/resources/about.glade +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - False - 5 - About PyLocator - True - center - 320 - 400 - dialog - - - True - False - 20 - - - True - False - 10 - - - True - False - pylocator.ico - 6 - - - True - True - 0 - - - - - True - False - - - True - False - <big><b>PyLocator </b></big> - - True - center - - - True - True - 0 - - - - - True - False - Version __version__ - True - center - - - True - True - 1 - - - - - True - True - 1 - - - - - False - False - 0 - - - - - True - True - automatic - automatic - - - True - True - False - word - textbuffer1 - - - - - True - True - 1 - - - - - True - False - - - Visit the PyLocator website - True - True - True - False - none - http://pylocator.thorstenkranz.de - - - True - False - 0 - - - - - False - False - 2 - - - - - True - False - True - center - - - gtk-close - True - True - True - False - True - - - True - True - 0 - - - - - False - False - end - 2 - - - - - - button1 - - - - Localization of EEG electrodes in MRI recordings. - -PyLocator has been mainly developed by Thorsten Kranz (thorstenkranz@gmail.com). Please visit our website for more information, documentation und a tutorial. - -Thank you for using PyLocator! - - diff --git a/pylocator/resources/camera24.png b/pylocator/resources/camera24.png deleted file mode 100644 index f852a4a..0000000 Binary files a/pylocator/resources/camera24.png and /dev/null differ diff --git a/pylocator/resources/camera48.png b/pylocator/resources/camera48.png deleted file mode 100644 index 21f34c7..0000000 Binary files a/pylocator/resources/camera48.png and /dev/null differ diff --git a/pylocator/resources/editCoordinates.glade b/pylocator/resources/editCoordinates.glade deleted file mode 100644 index cfdb083..0000000 --- a/pylocator/resources/editCoordinates.glade +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - False - 5 - Edit coordinates - True - center - 320 - dialog - - - True - False - 2 - - - True - False - end - - - gtk-cancel - True - True - True - False - True - - - False - False - 0 - - - - - gtk-ok - True - True - True - False - True - - - False - False - 1 - - - - - False - True - end - 0 - - - - - True - False - 10 - - - True - False - gtk-preferences - 6 - - - True - True - 0 - - - - - True - False - Please enter new coordinates -for the selected marker - center - - - - - - True - True - 1 - - - - - True - True - 1 - - - - - True - False - 3 - 2 - - - True - False - 0.89999997615814209 - X = - - - - - - - - True - False - 0.89999997615814209 - Y = - - - - - - 1 - 2 - - - - - True - False - 0.89999997615814209 - Z = - - - - - - 2 - 3 - - - - - True - True - • - False - False - True - True - - - 1 - 2 - - - - - True - True - • - False - False - True - True - - - 1 - 2 - 1 - 2 - - - - - True - True - • - False - False - True - True - - - 1 - 2 - 2 - 3 - - - - - True - True - 2 - - - - - - cancelButton - button1 - - - diff --git a/pylocator/resources/editLabel.glade b/pylocator/resources/editLabel.glade deleted file mode 100644 index 813d683..0000000 --- a/pylocator/resources/editLabel.glade +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - False - 5 - Edit label - True - center - 320 - 200 - dialog - - - True - False - 2 - - - True - False - end - - - gtk-cancel - True - True - True - False - True - - - False - False - 0 - - - - - gtk-ok - True - True - True - False - True - - - False - False - 1 - - - - - False - True - end - 0 - - - - - True - False - 10 - - - True - False - gtk-preferences - 6 - - - True - True - 0 - - - - - True - False - Please enter a label -for the selected marker - center - - - - - - True - True - 1 - - - - - True - True - 1 - - - - - True - True - 20 - • - False - False - True - True - - - True - True - 2 - - - - - - cancelButton - button1 - - - diff --git a/pylocator/resources/editSettings.glade b/pylocator/resources/editSettings.glade deleted file mode 100644 index 19444aa..0000000 --- a/pylocator/resources/editSettings.glade +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - False - 5 - Edit settings - True - center - 320 - dialog - - - True - False - 10 - - - True - False - 10 - - - True - False - gtk-preferences - 6 - - - True - True - 0 - - - - - True - False - Adjust some properties -of visualization - center - - - - - - True - True - 1 - - - - - False - False - 0 - - - - - True - False - 0 - - - True - False - 5 - - - True - False - - - True - False - 0.10000000149011612 - - - True - False - 0.10000000149011612 - 0.69999998807907104 - Opacity - - - - - - - - False - False - 0 - - - - - 150 - True - True - marker_opacity - 1 - - - - False - False - end - 1 - - - - - True - True - 0 - - - - - True - False - - - True - False - 0.10000000149011612 - - - True - False - 0.10000000149011612 - Default Color - - - - - - - - False - False - 0 - - - - - 150 - True - True - True - False - Default Color for Markers - #00000000ffff - - - - False - False - end - 1 - - - - - True - True - 1 - - - - - True - False - - - True - False - 0.10000000149011612 - - - True - False - 0.10000000149011612 - 0.69999998807907104 - Size of Markers - - - - - - - - False - False - 0 - - - - - 150 - True - True - marker_size - 1 - False - - - - False - False - end - 1 - - - - - True - True - 2 - - - - - - - True - False - Marker Settings - True - - - - - True - True - 1 - - - - - True - False - 0 - - - True - False - - - True - False - - - True - False - 0.10000000149011612 - - - True - False - 0.10000000149011612 - 0.69999998807907104 - Opacity of Planes - - - - - - - - False - False - 0 - - - - - 150 - True - True - planes_opacity - 1 - - - - False - False - end - 1 - - - - - True - True - 0 - - - - - - - True - False - Other settings - True - - - - - True - True - 2 - - - - - True - False - end - - - gtk-close - True - True - True - False - True - - - - False - False - 0 - - - - - False - True - end - 3 - - - - - - button1 - - - - 1 - 1 - 0.01 - 0.01 - - - 0.10000000000000001 - 10 - 3 - 0.10000000000000001 - 1 - - - 1 - 1 - 0.01 - 0.01 - - diff --git a/pylocator/resources/mainWindow.glade b/pylocator/resources/mainWindow.glade deleted file mode 100644 index 562f1e8..0000000 --- a/pylocator/resources/mainWindow.glade +++ /dev/null @@ -1,605 +0,0 @@ - - - - - - True - False - gtk-properties - - - True - False - gtk-media-forward - - - True - False - gtk-media-rewind - - - True - False - gtk-revert-to-saved - - - True - False - gtk-bold - - - False - 1 - PyLocator - center - 800 - 600 - pylocator.ico - - - - - True - False - - - True - False - True - - - True - False - False - _Markers - True - - - True - False - True - - - gtk-open - True - False - False - True - True - - - - - - gtk-save - True - False - False - True - True - - - - - - gtk-save-as - True - False - False - True - True - - - - - - True - False - False - - - - - gtk-quit - True - False - False - True - True - - - - - - - - - - True - False - False - Interaction _Mode - True - - - True - False - True - - - True - False - False - _Mouse Interact - True - True - True - - - - - - - True - False - False - _VTK Interact - True - True - mouseInteractMenuItem - - - - - - True - False - False - Set _Label - True - True - mouseInteractMenuItem - - - - - - True - False - False - _Select Markers - True - True - mouseInteractMenuItem - - - - - - True - False - False - Set _Color - True - True - mouseInteractMenuItem - - - - - - True - False - False - _Move Markers - True - True - mouseInteractMenuItem - - - - - - True - False - False - _Delete Markers - True - True - mouseInteractMenuItem - - - - - - - - - - True - False - False - _Tools - True - - - True - False - True - - - Align surface view to planes view - True - False - False - image6 - False - - - - - - Align planes view to surface view - True - False - False - image7 - False - - - - - - Reset cameras - True - False - False - image8 - False - - - - - - Toggle Labels - True - False - False - image9 - False - - - - - - Settings - True - False - False - image1 - False - - - - - - - - - - True - False - False - _Help - True - - - True - False - True - - - gtk-about - True - False - False - True - True - - - - - - - - - - False - True - 0 - - - - - True - True - 150 - True - - - True - True - - - True - False - 20 - - - True - False - markers.png - - - False - False - 0 - - - - - How can I mark positions? - True - True - True - 1 - False - none - 0.05000000074505806 - http://pylocator.thorstenkranz.de/faq.html#how-can-i-insert-markers-for-my-mri - - - False - False - end - 1 - - - - - - - True - False - Markers - - - False - - - - - True - False - 20 - - - True - False - surfaces.png - - - False - False - 0 - - - - - How can I reconstruct surfaces? - True - True - True - True - 1 - False - none - 0.05000000074505806 - http://pylocator.thorstenkranz.de/faq.html#how-can-i-reconstruct-iso-surfaces - - - False - False - end - 1 - - - - - 1 - - - - - True - False - 0 - Surfaces - - - 1 - False - - - - - True - False - 20 - - - True - False - rois.png - - - False - False - 0 - - - - - How can I render ROIs? - True - True - True - 1 - False - none - 0.05000000074505806 - http://pylocator.thorstenkranz.de/faq.html#how-can-i-render-regions-of-interest - - - False - False - end - 1 - - - - - 2 - - - - - True - False - ROI - - - 2 - False - - - - - True - False - 20 - - - How can I export images? - True - True - True - 1 - False - none - 0.05000000074505806 - http://pylocator.thorstenkranz.de/tutorial.html - - - False - False - end - 0 - - - - - True - False - screenshots.png - - - False - False - 1 - - - - - 3 - - - - - True - False - Screenshots - - - 3 - False - - - - - False - True - - - - - True - True - 300 - True - - - True - True - 325 - True - - - - - - - - - False - True - - - - - True - False - 1 - - - - - - - - - - - - True - True - - - - - False - True - - - - - True - True - end - 1 - - - - - - diff --git a/pylocator/resources/markers.png b/pylocator/resources/markers.png deleted file mode 100644 index d754550..0000000 Binary files a/pylocator/resources/markers.png and /dev/null differ diff --git a/pylocator/resources/pylocator.ico b/pylocator/resources/pylocator.ico deleted file mode 100644 index 2cd7b26..0000000 Binary files a/pylocator/resources/pylocator.ico and /dev/null differ diff --git a/pylocator/resources/rois.png b/pylocator/resources/rois.png deleted file mode 100644 index b831f18..0000000 Binary files a/pylocator/resources/rois.png and /dev/null differ diff --git a/pylocator/resources/screenshots.png b/pylocator/resources/screenshots.png deleted file mode 100644 index 78951af..0000000 Binary files a/pylocator/resources/screenshots.png and /dev/null differ diff --git a/pylocator/resources/surfaces.png b/pylocator/resources/surfaces.png deleted file mode 100644 index 5b4d5e8..0000000 Binary files a/pylocator/resources/surfaces.png and /dev/null differ diff --git a/pylocator/roi_renderer_props.py b/pylocator/roi_renderer_props.py deleted file mode 100644 index 0f1d666..0000000 --- a/pylocator/roi_renderer_props.py +++ /dev/null @@ -1,214 +0,0 @@ -from __future__ import division -import os.path -import gobject -import gtk -from gtkutils import error_msg, ButtonAltLabel - -from events import EventHandler -from shared import shared - -from surf_params import SurfParams - -from list_toolbar import ListToolbar -from colors import ColorChooser -from vtkNifti import vtkNiftiImageReader -from rois import RoiParams - -class RoiRendererProps(gtk.VBox): - SCROLLBARSIZE = 150,20 - lastColor = SurfParams.color - paramd = {} # a dict from names to SurfParam instances - - def __init__(self): - gtk.VBox.__init__(self) - self.show() - - toolbar = self.__create_toolbar() - self.pack_start(toolbar,False,False) - - self.scrolled_window = gtk.ScrolledWindow() - - self.inner_vbox = gtk.VBox() - self.inner_vbox.set_spacing(20) - self.scrolled_window.add_with_viewport(self.inner_vbox) - self.pack_start(self.scrolled_window) - self.scrolled_window.show_all() - - self._make_roi_list() - - button = ButtonAltLabel('Render', gtk.STOCK_EXECUTE) - button.show() - button.connect('clicked', self.render) - self.pack_end(button, False, False) - self.__update_treeview_visibility() - - - def render(self, *args): - EventHandler().notify("render now") - - def __create_toolbar(self): - conf = [ - [gtk.STOCK_ADD, - 'Add', - 'Load ROI from file and render it', - self.add_roi - ], - [gtk.STOCK_REMOVE, - 'Remove', - 'Remove selected ROI', - self.rm_roi - ], - #"-", - #[gtk.STOCK_GO_UP, - # 'Move up', - # 'Move selected marker up in list', - # self.cb_move_up - #], - #[gtk.STOCK_GO_DOWN, - # 'Move down', - # 'Move selected marker down in list', - # self.cb_move_down - #], - ] - return ListToolbar(conf) - - def _make_roi_list(self): - #create TreeView - #Fields: Index, Short filename, long FN, is_active?, opacity - self.tree_roi = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING,gobject.TYPE_BOOLEAN,gobject.TYPE_FLOAT) - self.nroi = 0 - self.treev_roi = gtk.TreeView(self.tree_roi) - self.treev_sel = self.treev_roi.get_selection() - self.treev_sel.connect("changed",self.treev_sel_changed) - self.treev_sel.set_mode(gtk.SELECTION_SINGLE) - renderer = gtk.CellRendererText() - renderer.set_property("xalign",0.0) - #renderer.set_xalign(0.0) - #self.col1 = gtk.TreeViewColumn("#",renderer,text=0) - #self.treev_roi.append_column(self.col1) - self.col1 = gtk.TreeViewColumn("Short filename",renderer,text=1) - self.treev_roi.append_column(self.col1) - #self.treev_roi.show() - self.inner_vbox.pack_start(self.treev_roi, False) - - #Empty-indicator - self.emptyIndicator = gtk.Label('No region-of-interest defined') - self.emptyIndicator.show() - self.inner_vbox.pack_start(self.emptyIndicator, False) - - #Edit properties of one ROI - self.props_frame = self._make_properties_frame() - self.inner_vbox.pack_start(self.props_frame,False,False) - - #vboxProps.pack_start() - - def _make_properties_frame(self): - frame = gtk.Frame('Properties') - frame.set_border_width(5) - vboxProps = gtk.VBox() - frame.add(vboxProps) - f1 = gtk.Frame("Opacity") - f1.show() - vboxProps.pack_start(f1, False) - self.scrollbar_opacity = gtk.HScrollbar() - self.scrollbar_opacity.set_update_policy(gtk.UPDATE_DELAYED) - self.scrollbar_opacity.show() - self.scrollbar_opacity.set_size_request(*self.SCROLLBARSIZE) - self.scrollbar_opacity.set_range(0, 1) - self.scrollbar_opacity.set_increments(.05, .2) - self.scrollbar_opacity.set_value(1.0) - self.scrollbar_opacity.connect('value_changed', self.change_opacity_of_roi) - f1.add(self.scrollbar_opacity) - - f2 = gtk.Frame("Color") - f2.show() - vboxProps.pack_start(f2, False) - tmp = gtk.HBox() - f2.add(tmp) - self.color_chooser = ColorChooser() - self.color_chooser.connect("color_changed",self.change_color_of_roi) - tmp.pack_start(self.color_chooser,True,False) - vboxProps.show_all() - return frame - - def add_roi(self,*args): - dialog = gtk.FileSelection('Choose filename for ROI mask') - dialog.set_filename(shared.get_last_dir()) - dialog.show() - response = dialog.run() - if response==gtk.RESPONSE_OK: - fname = dialog.get_filename() - dialog.destroy() - try: - #Actually add ROI - self.nroi+=1 - tree_iter = self.tree_roi.append(None) - self.tree_roi.set(tree_iter,0,self.nroi,1,os.path.split(fname)[1],2,fname,3,True) - self.__update_treeview_visibility() - - roi_image_reader = vtkNiftiImageReader() - roi_image_reader.SetFileName(fname) - roi_image_reader.Update() - roi_id = self.tree_roi.get(tree_iter,0) - self.paramd[roi_id] = RoiParams(roi_image_reader.GetOutput()) - #self.paramd[roi_id].update_pipeline() - #print self.paramd[roi_id].intensity - shared.set_file_selection(fname) - except IOError: - error_msg( - 'Could not load ROI mask from %s' % fname, - ) - finally: - self.__update_treeview_visibility() - else: dialog.destroy() - self.render() - - def rm_roi(self,*args): - treestore,treeiter = self.treev_sel.get_selected() - roi_id = treestore.get(treeiter,0) - treestore.remove(treeiter) - self.paramd[roi_id].destroy() - del self.paramd[roi_id] - self.__update_treeview_visibility() - self.render() - - def treev_sel_changed(self,selection): - treeiter = selection.get_selected()[1] - if treeiter: - self.props_frame.show_all() - #print "selection changed", self.tree_roi.get(treeiter,0,1,2) - roi_id = self.tree_roi.get(treeiter,0) - try: - self.color_chooser._set_color(self.paramd[roi_id].color) - except Exception, e: - print "During setting color of color chooser:", type(e),e - try: - self.scrollbar_opacity.set_value(self.paramd[roi_id].opacity) - except Exception, e: - print "During setting value of opacity scrollbar:", type(e),e - else: - self.props_frame.hide() - - def change_color_of_roi(self,*args): - treeiter = self.treev_sel.get_selected()[1] - if treeiter: - roi_id = self.tree_roi.get(treeiter,0) - self.paramd[roi_id].set_color(self.color_chooser.color) - self.render() - - def change_opacity_of_roi(self,*args): - treeiter = self.treev_sel.get_selected()[1] - if treeiter: - roi_id = self.tree_roi.get(treeiter,0) - self.paramd[roi_id].set_opacity(self.scrollbar_opacity.get_value()) - self.render() - - def __update_treeview_visibility(self): - if self.tree_roi.get_iter_first()==None: - # tree is empty - self.emptyIndicator.show() - self.treev_roi.hide() - else: - self.emptyIndicator.hide() - self.treev_roi.show() - diff --git a/pylocator/rois.py b/pylocator/rois.py deleted file mode 100644 index cb2726f..0000000 --- a/pylocator/rois.py +++ /dev/null @@ -1,129 +0,0 @@ -from __future__ import division -import vtk -from events import EventHandler -from surf_params import SurfParams - - -class RoiParams(SurfParams): - intensity = 0.5 - - def __init__(self, imageData, color=None): - if color==None: - color = (0.,0.,1.) - SurfParams.__init__(self, imageData, self.intensity, color) - - def set_lighting(self): - surf_prop = self.isoActor.GetProperty() - surf_prop.SetAmbient(.2) - surf_prop.SetDiffuse(.3) - surf_prop.SetSpecular(.5) - - def notify_add_surface(self, pipe): - EventHandler().notify("add roi", self._uuid, pipe, self._color) - - def notify_remove_surface(self): - EventHandler().notify("remove roi", self._uuid) - - def notify_color_surface(self, color): - EventHandler().notify("color roi", self._uuid, color) - - def notify_change_surface_opacity(self, opacity): - EventHandler().notify("change roi opacity", self._uuid, opacity) - -class RoiEdgeActor(vtk.vtkActor): - def __init__(self, roi_pipe, color, planeWidget, transform=None, lineWidth=2): - self.roiPipe = roi_pipe - self._color = color - self.planeWidget = planeWidget - self.transform = transform - self._line_width=lineWidth - - self.__create_and_set_mapper() - - lp = self.GetProperty() - lp.SetRepresentationToWireframe() - lp.SetAmbient(1.0) - lp.SetColor(self._color) - lp.SetLineWidth(lineWidth) - self.SetProperty(lp) - self.VisibilityOff() - - if transform is not None: - self.filter = vtk.vtkTransformPolyDataFilter() - self.filter.SetTransform(transform) - else: - self.filter = None - - self.update() - - def __create_and_set_mapper(self): - self.implicitPlane = vtk.vtkPlane() - self.edges = vtk.vtkCutter() - self.strips = vtk.vtkStripper() - self.poly = vtk.vtkPolyData() - self.mapper = vtk.vtkPolyDataMapper() - - self.edges.SetInputConnection(self.roiPipe.GetOutputPort()) - self.implicitPlane.SetNormal(self.planeWidget.GetNormal()) - self.implicitPlane.SetOrigin(self.planeWidget.GetOrigin()) - - self.edges.SetCutFunction(self.implicitPlane) - self.edges.GenerateCutScalarsOff() - self.edges.SetValue(0, 0.0) - self.strips.SetInputConnection(self.edges.GetOutputPort()) - self.strips.Update() - self.poly.SetPoints(self.strips.GetOutput().GetPoints()) - self.poly.SetPolys(self.strips.GetOutput().GetLines()) - self.mapper.SetInput(self.poly) - self.mapper.ScalarVisibilityOff() - self.SetMapper(self.mapper) - - def update(self, *args): - # side effects update the poly - if not self.is_visible(): return 0 - - if self.filter is not None: - self.filter.SetInput(self.poly) - self.mapper.SetInputConnection(self.filter.GetOutputPort()) - else: - self.mapper.SetInput(self.poly) - self.mapper.Update() - self.VisibilityOn() - return 1 - - def get_roi_param(self): - return self.marker - - def get_line_width(self): - return self._line_width - - def set_line_width(self, w): - self._line_width = w - self.GetProperty().SetLineWidth(w) - - line_width = property(get_line_width, set_line_width) - - def get_color(self): - return self._color - - def set_color(self, color): - self._color = color - self.GetProperty().SetColor(color) - - color = property(get_color, set_color) - - - def is_visible(self): - # side effects update the poly; so kill me - self.implicitPlane.SetNormal(self.planeWidget.GetNormal()) - self.implicitPlane.SetOrigin(self.planeWidget.GetOrigin()) - self.strips.Update() - self.poly.SetPoints(self.strips.GetOutput().GetPoints()) - self.poly.SetPolys(self.strips.GetOutput().GetLines()) - self.poly.Update() - return self.poly.GetNumberOfPolys() - - def silly_hack(self, *args): - # vtk strips my attributes if I don't register a func as an observer - pass - diff --git a/pylocator/screenshot_props.py b/pylocator/screenshot_props.py deleted file mode 100644 index b22bf76..0000000 --- a/pylocator/screenshot_props.py +++ /dev/null @@ -1,176 +0,0 @@ -import gtk -from gtkutils import error_msg -from resources import camera_small_fn -from shared import shared - -INTERACT_CURSOR, MOVE_CURSOR, COLOR_CURSOR, SELECT_CURSOR, DELETE_CURSOR, LABEL_CURSOR, SCREENSHOT_CURSOR = gtk.gdk.ARROW, gtk.gdk.HAND2, gtk.gdk.SPRAYCAN, gtk.gdk.TCROSS, gtk.gdk.X_CURSOR, gtk.gdk.PENCIL, gtk.gdk.ICON - -class ScreenshotProps(gtk.VBox): - """ - CLASS: ScreenshotProps - DESC: - """ - - def __init__(self): - self._sts = [] # Stores all PyLocatorRenderWindows to invoke on button press - gtk.VBox.__init__(self) - - self.scrolled_window = gtk.ScrolledWindow() - self.inner_vbox = gtk.VBox() - self.inner_vbox.set_spacing(20) - self.scrolled_window.add_with_viewport(self.inner_vbox) - self.pack_start(self.scrolled_window) - self.scrolled_window.show_all() - - self.__make_filename_frame() - self.__make_magnification_frame() - self.__make_buttons_frame() - self.__make_explanation_frame() - - self.show() - - - def __make_filename_frame(self): - frame = gtk.Frame('Filename pattern ') - frame.set_border_width(5) - frame.show() - - vbox = gtk.VBox() - frame.add(vbox) - vbox.show() - - self.entryFn = gtk.Entry() - self.entryFn.show() - self.entryFn.set_text('pylocator%03i.png') - vbox.pack_start(self.entryFn,False,False) - - self.buttonPropose = gtk.Button(label="Propose!") - self.buttonPropose.show() - self.buttonPropose.connect('clicked', self.propose_fn) - vbox.pack_start(self.buttonPropose, True, False) - - self.inner_vbox.pack_start(frame,False,False) - - def __make_magnification_frame(self): - frame = gtk.Frame('Magnification') - frame.set_border_width(5) - frame.show() - adjustment = gtk.Adjustment(2, 1, 10, 1, 1, 0) - self.sbMag = gtk.SpinButton(adjustment) - self.sbMag.show() - frame.add(self.sbMag) - self.inner_vbox.pack_start(frame,False,False) - - def __make_explanation_frame(self): - frame = gtk.Frame('Hints') - frame.set_border_width(10) - frame.show() - - hbox = gtk.HBox() - hbox.show() - frame.add(hbox) - - self.explain = gtk.Label() - self.explain.set_line_wrap(True) - self.explain.set_markup( - """ - -Set a filename pattern to use for screenshots. Needs a "%03i" for automatic numbering. - -Use the buttons above to take a screenshot of one of the 3d-widgets. Alternatively, take screenshots of all widgets using the button below. - -Set the magnification factor to increase image size for screenshots. - -""") - self.explain.set_size_request(200,-1) - self.explain.show() - hbox.pack_start(self.explain, False, False, 10) - - self.inner_vbox.pack_start(frame,False,False) - - def __make_buttons_frame(self): - #Frame for buttons for screenshots - frame = gtk.Frame('Take screenshot of') - frame.set_border_width(5) - frame.show() - - self.buttons_vbox = gtk.VBox() - frame.add(self.buttons_vbox) - - self.inner_vbox.pack_start(frame,False,False,padding=10) - - def create_buttons(self): - def make_button(pixbuf): - camera = gtk.Image() - camera.set_from_pixbuf(pixbuf) - camera.show() - button = gtk.Button(label=None) - button.set_image(camera) - return button - - def insert_row(txt, pixbuf, row): - label = gtk.Label(txt) - label.show() - hbox=gtk.HBox() - hbox.show() - hbox.pack_end(label,False,False) - button = make_button(pixbuf) - button.show() - table.attach(hbox,0,1,row,row+1,xoptions=gtk.FILL,yoptions=0) - table.attach(button,1,2,row,row+1,xoptions=gtk.FILL,yoptions=0) - return label, button - - table = gtk.Table(len(self._sts)+1,2) - table.set_col_spacings(10) - table.set_row_spacings(3) - table.show() - self.buttons_vbox.pack_start(table,padding=10) - - cameraPixBuf = gtk.gdk.pixbuf_new_from_file(camera_small_fn) - # button for all screenshots - label, button = insert_row("All views",cameraPixBuf,0) - button.connect('clicked', self.take_all_shots) - - for i,st in enumerate(self._sts): - label, button = insert_row( - st.screenshot_button_label, - cameraPixBuf, i+1 - ) - button.connect('clicked',self.take_shot,i) - - self.buttons_vbox.show() - - def propose_fn(self,*args): - mri_fn = shared.lastSel - if len(mri_fn) == 0: - error_msg("Cannot propose filename: \nFilename of MRI unknown") - return False - for suff in [".nii.gz",".nii"]: - if mri_fn.endswith(suff): - mri_fn = mri_fn[:-len(suff)] - self.entryFn.set_text(mri_fn+"_pylocator%03i.png") - return True - - def append_screenshot_taker(self,st): - self._sts.append(st) - - def take_shot(self, button, idx): - """For one PyLocatorRenderWindow, take a SS""" - #print "take_shot", args - if len(self._sts)==0: - error_msg("Cannot take screenshots: \nNo instances registered.") - return False - fn_pattern = self.entryFn.get_text() - mag = self.sbMag.get_value() - self._sts[idx].take_screenshot(fn_pattern, mag) - - def take_all_shots(self, *args): - """For each PyLocatorRenderWindow in list, take a SS""" - if len(self._sts)==0: - error_msg("Cannot take screenshots: \nNo instances registered.") - return False - fn_pattern = self.entryFn.get_text() - mag = self.sbMag.get_value() - for st in self._sts: - st.take_screenshot(fn_pattern, mag) - diff --git a/pylocator/shared.py b/pylocator/shared.py deleted file mode 100644 index 536f6ae..0000000 --- a/pylocator/shared.py +++ /dev/null @@ -1,37 +0,0 @@ -import os - -class Shared(object): - debug = False - lastSel = '' - labels = ["L","R","P","A","I","S"] - #lastSel = os.getcwd() + os.sep - ratio = 3 - #screenshot_magnification = 3 - screenshot_cnt = 1 - - planes_opacity = 1. - markers_opacity = 1. - marker_size = 3. - - lastLabel = "" - - def set_file_selection(self, name): - """ - Set the filename or dir of the most recent file selected - """ - self.lastSel = name - - def get_last_dir(self): - """ - Return the dir name of the most recent file selected - """ - if os.path.isdir(self.lastSel): - return self.lastSel - else: - return os.path.dirname(self.lastSel) + os.sep - - - - - -shared = Shared() diff --git a/pylocator/surf_params.py b/pylocator/surf_params.py deleted file mode 100644 index 6bc9bfa..0000000 --- a/pylocator/surf_params.py +++ /dev/null @@ -1,134 +0,0 @@ -from __future__ import division -import uuid -import vtk -import gtk -from gtkutils import ProgressBarDialog -from events import EventHandler -from connect_filter import ConnectFilter -from decimate_filter import DecimateFilter -from colors import colorSeq, gdkColor2tuple - -class SurfParams(object): - label = "Surface" - colorName, color_ = colorSeq[0] - intensity = 80. - _opacity = 1.0 - - useConnect = True - useDecimate = False - - def __init__(self, imageData, intensity, color=None): - self._uuid = uuid.uuid1() - if intensity!=None: - self.intensity = intensity - if color==None: - color=self.color_ - self.set_color(color) - - self.connect = ConnectFilter() - self.deci = DecimateFilter() - self.marchingCubes = vtk.vtkMarchingCubes() - self.marchingCubes.SetInput(imageData) - - self.output = vtk.vtkPassThrough() - - self.prog = ProgressBarDialog( - title='Rendering surface %s' % self.label, - parent=None, - msg='Marching cubes ....', - size=(300,40), - ) - self.prog.set_modal(True) - - def start(o, event): - self.prog.show() - while gtk.events_pending(): gtk.main_iteration() - - - def progress(o, event): - val = o.GetProgress() - self.prog.bar.set_fraction(val) - while gtk.events_pending(): gtk.main_iteration() - - def end(o, event): - self.prog.hide() - while gtk.events_pending(): gtk.main_iteration() - - self.marchingCubes.AddObserver('StartEvent', start) - self.marchingCubes.AddObserver('ProgressEvent', progress) - self.marchingCubes.AddObserver('EndEvent', end) - - self.update_pipeline() - - self.notify_add_surface(self.output) - - - def update_pipeline(self): - pipe = self.marchingCubes - - if self.useConnect: - self.connect.SetInputConnection( pipe.GetOutputPort()) - pipe = self.connect - - if self.useDecimate: - self.deci.SetInputConnection( pipe.GetOutputPort()) - pipe = self.deci - - self.output.SetInputConnection( pipe.GetOutputPort() ) - self.update_properties() - - def notify_add_surface(self, pipe): - EventHandler().notify("add surface", self._uuid, pipe, self._color) - - def notify_remove_surface(self): - EventHandler().notify("remove surface", self._uuid) - - def notify_color_surface(self, color): - EventHandler().notify("color surface", self._uuid, color) - - def notify_change_surface_opacity(self, opacity): - EventHandler().notify("change surface opacity", self._uuid, opacity) - - def update_properties(self): - self.marchingCubes.SetValue(0, self.intensity) - self.notify_color_surface(self.color) - - if self.useConnect: self.connect.update() - if self.useDecimate: self.deci.update() - - def update_viewer(self, event, *args): - if event=='set image data': - imageData = args[0] - self.set_image_data(imageData) - - def __del__(self): - self.notify_remove_surface() - - def set_color(self,color, color_name=""): - #print color, type(color) - self.colorName = color_name - if type(color)==gtk.gdk.Color: - self._color = gdkColor2tuple(color) - else: - self._color = color - self.notify_color_surface(self._color) - - def get_color(self): - return self._color - - def set_opacity(self,opacity): - self._opacity = opacity - self.notify_change_surface_opacity(self._opacity) - - def get_opacity(self): - return self._opacity - - def get_uuid(self): - return self._uuid - - color = property(get_color,set_color) - opacity = property(get_opacity,set_opacity) - uuid = property(get_uuid) - - def destroy(self): - self.notify_remove_surface() diff --git a/pylocator/surf_renderer.py b/pylocator/surf_renderer.py deleted file mode 100644 index 16bb06b..0000000 --- a/pylocator/surf_renderer.py +++ /dev/null @@ -1,129 +0,0 @@ -from __future__ import division -import vtk -import gtk -from gtkutils import error_msg - -from events import EventHandler -from markers import Marker -from shared import shared -from render_window import PyLocatorRenderWindow, ThreeDimRenderWindow - -class SurfRenderWindow(ThreeDimRenderWindow, PyLocatorRenderWindow): - picker_id = None - - def __init__(self, imageData=None): - PyLocatorRenderWindow.__init__(self) - ThreeDimRenderWindow.__init__(self) - self.surface_actors = {} - self.AddObserver('KeyPressEvent', self.key_press) - - def set_image_data(self, imageData): - self.imageData = imageData - if imageData is None: return - center = imageData.GetCenter() - #spacing = imageData.GetSpacing() - bounds = imageData.GetBounds() - pos = center[0], center[1], center[2] - max(bounds)*2 - fpu = center, pos, (0,-1,0) - self.set_camera(fpu) - self.camera = self.renderer.GetActiveCamera() - - def set_labels_visibility(self, visible=True): - if visible: - actors = self.textActors.values() - for actor in actors: - actor.VisibilityOn() - else: - actors = self.textActors.values() - for actor in actors: - actor.VisibilityOff() - - def set_picker_surface(self, uuid): - self.picker_id = uuid - - def add_surface(self, uuid, pipe, color): - isoActor = self._create_actor(pipe) - isoActor.GetProperty().SetColor(color) - self._set_surface_lighting(isoActor) - self.surface_actors[uuid] = isoActor - if not self.picker_id: - self.picker_id = uuid - - def remove_surface(self, uuid): - actor = self.__get_surface_actor(uuid) - if actor: - self.renderer.RemoveActor(actor) - del self.surface_actors[uuid] - - def color_surface(self, uuid, color): - actor = self.__get_surface_actor(uuid) - if actor: - actor.GetProperty().SetColor(color) - - def change_surface_opacity(self, uuid, opacity): - actor = self.__get_surface_actor(uuid) - if actor: - actor.GetProperty().SetOpacity(opacity) - - def __get_surface_actor(self, uuid): - if not self.surface_actors.has_key(uuid): - return - return self.surface_actors[uuid] - - def update_viewer(self, event, *args): - PyLocatorRenderWindow.update_viewer(self, event, *args) - if event=='set picker surface': - self.set_picker_surface(args[0]) - self.Render() - - def key_press(self, interactor, event): - if shared.debug: print "key press event in SurfRenderWindow" - key = interactor.GetKeySym() - sas = self.surface_actors - - def checkPickerId(): - if not self.picker_id: - error_msg('Cannot insert marker. Choose surface first.') - return False - return True - - if key.lower()=='q': #hehehe - gtk.main_quit() - if key.lower()=='i': - if not checkPickerId(): - return - if shared.debug: print "Inserting Marker" - x,y = interactor.GetEventPosition() - picker = vtk.vtkCellPicker() - picker.PickFromListOn() - actor = self.__get_surface_actor(self.picker_id) - if actor==None: return - picker.AddPickList(actor) - picker.SetTolerance(0.005) - picker.Pick(x, y, 0, self.renderer) - points = picker.GetPickedPositions() - numPoints = points.GetNumberOfPoints() - if numPoints<1: return - pnt = points.GetPoint(0) - - marker = Marker(xyz=pnt, - rgb=EventHandler().get_default_color(), - radius=shared.ratio*shared.marker_size) - EventHandler().add_marker(marker) - elif key.lower()=='x': - #if not checkPickerIdx(): - # return - x,y = interactor.GetEventPosition() - picker = vtk.vtkCellPicker() - picker.PickFromListOn() - for actor in sas: - picker.AddPickList(actor) - picker.SetTolerance(0.01) - picker.Pick(x, y, 0, self.renderer) - cellId = picker.GetCellId() - if cellId==-1: - pass - else: - o = self.paramd.values()[0] - o.remove.RemoveCell(cellId) - interactor.Render() diff --git a/pylocator/surf_renderer_props.py b/pylocator/surf_renderer_props.py deleted file mode 100644 index 265c81b..0000000 --- a/pylocator/surf_renderer_props.py +++ /dev/null @@ -1,479 +0,0 @@ -from __future__ import division - -import gobject -import gtk - -from gtkutils import error_msg, ButtonAltLabel -from dialogs import edit_label - -from events import EventHandler - -from colors import ColorChooserWithPredefinedColors, colorSeq - -from list_toolbar import ListToolbar -from surf_params import SurfParams - -from decimate_filter import DecimateFilter -from connect_filter import ConnectFilter - -class SurfRendererProps(gtk.VBox): - SCROLLBARSIZE = 150,20 - lastColor = SurfParams.color_ - lastColorName = SurfParams.colorName - picker_surface_id = None - pickerIdx = None - imageData = None - ignore_settings_updates = False - - paramd = {} # a dict from ids (indices) to SurfParam instances - - def __init__(self): - EventHandler().attach(self) - self.nsurf=0 - - gtk.VBox.__init__(self) - self.show() - self.set_homogeneous(False) - - toolbar = self.__create_toolbar() - self.pack_start(toolbar,False,False) - toolbar.show() - - self.scrolled_window = gtk.ScrolledWindow() - self.scrolled_window.show() - self.inner_vbox = gtk.VBox() - self.inner_vbox.show() - self.inner_vbox.set_spacing(20) - self.scrolled_window.add_with_viewport(self.inner_vbox) - #self.scrolled_window.show() - #self.inner_vbox.show() - self.pack_start(self.scrolled_window) - - self._make_segment_list() - - def hide(*args): - self.hide() - return True - self.connect('delete_event', hide) - - button = ButtonAltLabel('Render', gtk.STOCK_EXECUTE) - button.show() - button.connect('clicked', self.render) - self.pack_start(button, False, False) - - def __create_toolbar(self): - conf = [ - [gtk.STOCK_ADD, - 'Add', - 'Create a new iso-surface with default settings', - self.add_segment - ], - [gtk.STOCK_REMOVE, - 'Remove', - 'Remove selected surface', - self.cb_remove - ], - "-", - [gtk.STOCK_BOLD, - 'name', - 'Edit name of surface', - self.cb_edit_name - ], - ] - return ListToolbar(conf) - - def _make_segment_list(self): - #create TreeView - #Fields: idx, Name - self.tree_surf = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING) - self.nroi = 0 - self.treev_surf = gtk.TreeView(self.tree_surf) - self.treev_sel = self.treev_surf.get_selection() - self.treev_sel.connect("changed",self.treev_sel_changed) - self.treev_sel.set_mode(gtk.SELECTION_SINGLE) - renderer = gtk.CellRendererText() - renderer.set_property("xalign",0.0) - #renderer.set_xalign(0.0) - self.col1 = gtk.TreeViewColumn("Name",renderer,text=1) - self.treev_surf.append_column(self.col1) - #self.treev_roi.show() - self.inner_vbox.pack_start(self.treev_surf, False) - #self.treev_surf.show() - - #Empty-indicator - self.emptyIndicator = gtk.Label('No segment defined') - self.emptyIndicator.show() - self.inner_vbox.pack_start(self.emptyIndicator, False) - - #Edit properties of one surface - self.props_frame = gtk.Frame('Properties') - self.props_frame.set_border_width(5) - self.inner_vbox.pack_start(self.props_frame,False,False) - vboxProps = gtk.VBox() - vboxProps.set_spacing(20) - vboxProps.show() - self.props_frame.add(vboxProps) - general_expander = self._make_general_properties_expander() - vboxProps.pack_start(general_expander, False) - pipeline_expander = self._make_pipeline_expander() - vboxProps.pack_start(pipeline_expander, False) - - def _make_general_properties_expander(self): - expander = gtk.Expander('General settings') - expander.set_expanded(True) - expander.show() - - vbox = gtk.VBox() - vbox.show() - vbox.set_spacing(5) - expander.add(vbox) - - frame = gtk.Frame("Threshold") - frame.show() - vbox.pack_start(frame) - scrollbar = gtk.HScrollbar() - scrollbar.set_update_policy(gtk.UPDATE_DELAYED) - #scrollbar.set_draw_value(True) - scrollbar.show() - scrollbar.set_size_request(*self.SCROLLBARSIZE) - scrollbar.set_range(0, 100) - scrollbar.set_increments(1, 5) - scrollbar.set_value(80) - scrollbar.connect('value-changed', self.change_threshold_of_surf) - self.scrollbar_threshold = scrollbar - frame.add(scrollbar) - - frame = gtk.Frame("Opacity") - frame.show() - vbox.pack_start(frame) - scrollbar = gtk.HScrollbar() - scrollbar.set_update_policy(gtk.UPDATE_DELAYED) - scrollbar.show() - scrollbar.set_size_request(*self.SCROLLBARSIZE) - scrollbar.set_range(0, 1) - scrollbar.set_increments(.05, .2) - scrollbar.set_value(1.0) - scrollbar.connect('value-changed', self.change_opacity_of_surf) - self.scrollbar_opacity = scrollbar - frame.add(scrollbar) - - hbox = gtk.HBox() - hbox.show() - vbox.pack_start(hbox, False) - frame = self._make_color_chooser_frame() - hbox.pack_start(frame, True, True) - frame = self._make_picker_frame() - hbox.pack_start(frame, True, True) - - self.visibility_toggle = gtk.ToggleButton("Visible") - vbox.pack_start(self.visibility_toggle, False, False) - - return expander - - def _make_color_chooser_frame(self): - frame = gtk.Frame("Color") - frame.show() - self.color_chooser = ColorChooserWithPredefinedColors(colorSeq) - self.color_chooser.connect("color_changed", self.change_color_of_surf) - frame.add(self.color_chooser) - return frame - - def _make_picker_frame(self): - frame = gtk.Frame("Insert markers") - frame.show() - self.pickerButton = gtk.ToggleButton("here!") - self.pickerButton.connect("toggled", self.set_surface_for_picking) - self.pickerButton.show() - frame.add(self.pickerButton) - return frame - - def _make_pipeline_expander(self): - def decimate_toggled(button): - frameDecimateFilter.set_sensitive(button.get_active()) - apply_() - - def connect_toggled(button): - frameConnectFilter.set_sensitive(button.get_active()) - apply_() - - def set_connect_mode(id_): - if self.paramd[id_].useConnect: - for num in self.connectExtractButtons.keys(): - bt = self.connectExtractButtons[num] - if bt.get_active(): - self.paramd[id_].connect.mode = num - break - - def set_decimate_params(id_): - if self.paramd[id_].useDecimate: - self.paramd[id_].deci.targetReduction = self.scrollbar_target_reduction.get_value() - - def connect_method_changed(button): - if button.get_active(): - apply_() - - def apply_(*args): - if self.ignore_settings_updates: - return - id_ = self.__get_selected_id() - self.paramd[id_].useConnect = self.buttonUseConnect.get_active() - self.paramd[id_].useDecimate = self.buttonUseDecimate.get_active() - - set_connect_mode(id_) - set_decimate_params(id_) - pa = self.paramd[id_] - pa.update_pipeline() - self.render() - - expander = gtk.Expander('Pipeline settings') - - vbox = gtk.VBox() - vbox.show() - vbox.set_spacing(3) - expander.add(vbox) - - self.vboxPipeline = vbox - - decattrs = DecimateFilter.labels.keys() - decattrs.sort() - self.decattrs = decattrs - - names = self.paramd.keys() - names.sort() - - # Filter selection - framePipelineFilters = gtk.Frame('Pipeline filters') - framePipelineFilters.set_border_width(5) - vbox.pack_start(framePipelineFilters, True, True) - vboxFrame = gtk.VBox() - vboxFrame.set_spacing(3) - framePipelineFilters.add(vboxFrame) - self.buttonUseConnect = gtk.CheckButton('Use connect filter') - self.buttonUseConnect.set_active(False) - self.buttonUseConnect.connect('toggled', connect_toggled) - vboxFrame.pack_start(self.buttonUseConnect, True, True) - self.buttonUseDecimate = gtk.CheckButton('Use decimate filter') - self.buttonUseDecimate.set_active(False) - self.buttonUseDecimate.connect('toggled', decimate_toggled) - vboxFrame.pack_start(self.buttonUseDecimate, True, True) - - #Connect filter settings - frameConnectFilter = gtk.Frame('Connect filter settings') - frameConnectFilter.set_border_width(5) - frameConnectFilter.set_sensitive(False) - vbox.pack_start(frameConnectFilter, True, True) - vboxFrame = gtk.VBox() - vboxFrame.set_spacing(3) - frameConnectFilter.add(vboxFrame) - extractModes = ConnectFilter.num2mode.items() - extractModes.sort() - lastButton = None - self.connectExtractButtons = {} - for num, name in extractModes: - button = gtk.RadioButton(lastButton) - button.set_label(name) - if num==ConnectFilter.mode: - button.set_active(True) - button.connect("toggled",connect_method_changed) - vboxFrame.pack_start(button, True, True) - self.connectExtractButtons[num] = button - lastButton = button - - #Decimate filter settings - frameDecimateFilter = gtk.Frame('Decimate filter settings') - frameDecimateFilter.set_border_width(5) - frameDecimateFilter.set_sensitive(False) - vbox.pack_start(frameDecimateFilter, True, True) - vboxFrame = gtk.VBox() - vboxFrame.set_spacing(3) - vboxFrame.pack_start(gtk.Label("Target Reduction")) - scrollbar = gtk.HScrollbar() - scrollbar.set_update_policy(gtk.UPDATE_DELAYED) - scrollbar.show() - scrollbar.set_size_request(*self.SCROLLBARSIZE) - scrollbar.set_range(0, 0.99) - scrollbar.set_increments(.05, .2) - scrollbar.set_value(0.8) - scrollbar.connect('value_changed', apply_) - self.scrollbar_target_reduction = scrollbar - vboxFrame.pack_start(scrollbar) - frameDecimateFilter.add(vboxFrame) - - expander.show_all() - return expander - - def render(self, *args): - EventHandler().notify("render now") - - def update_pipeline_params(self, *args): - id_ = self.__get_selected_id() - - # set the active props of the filter frames - self.buttonUseConnect.set_active(self.paramd[id_].useConnect) - self.buttonUseDecimate.set_active(self.paramd[id_].useDecimate) - connect_mode = self.paramd[id_].connect.mode - activeButton = self.connectExtractButtons[connect_mode] - activeButton.set_active(True) - - self.scrollbar_target_reduction.set_value(self.paramd[id_].deci.targetReduction) - - def add_segment(self, button): - if not self.imageData: - error_msg("Cannot create surface. Image data of surface renderer is not set.") - return - if self.nsurf==0: - self.__adjust_scrollbar_threshold_for_data() - self.nsurf +=1 - - intensity = self.__calculate_intensity_threshold() - if intensity is None: return - - name = self.__create_segment_name(self.nsurf) - if (not name) or name=="": - return - - tree_iter = self.tree_surf.append(None) - self.tree_surf.set(tree_iter, 0,self.nsurf, 1, name) - - self.__update_treeview_visibility() - - self.paramd[self.nsurf] = SurfParams(self.imageData, intensity, self.lastColor) - params = self.paramd[self.nsurf] - if self.nsurf==1: - self.picker_surface_id = params.uuid - params.label = name - params.intensity = intensity - params.set_color(self.lastColor, self.lastColorName) - params.update_properties() - - self.render() - - def __calculate_intensity_threshold(self): - min_, median_, max_ = EventHandler().get_nifti_stats() - return median_ - - def __create_segment_name(self, idx): - return "Surface %i" % idx - - def interaction_event(self, observer, event): - return - #if not self.collecting: return - #xyzv = [0,0,0,0] - #observer.GetCursorData(xyzv) - #self.add_intensity(xyzv[3]) - #self.entryIntensity.set_text('%1.1f' % (self.intensitySum/self.intensityCnt)) - - def __adjust_scrollbar_threshold_for_data(self): - valid_increments = sorted([1.*10**e for e in range(-2,3)] + - [2.*10**e for e in range(-2,3)] + - [5.*10**e for e in range(-2,3)] - ) - min_, median_, max_ = EventHandler().get_nifti_stats() - upper_limit = min(max_ , median_+4*(median_-min_)) #if max is too high - incr1 = [i for i in valid_increments if i<(upper_limit-min_)/100][-1] - incr2 = [i for i in valid_increments if i<(upper_limit-min_)/20][-1] - self.scrollbar_threshold.set_range(min_, upper_limit) - self.scrollbar_threshold.set_increments(incr1, incr2) - self.scrollbar_threshold.set_value(median_) - - def __get_selected_id(self): - treeiter = self.treev_sel.get_selected()[1] - if treeiter: - return self.tree_surf.get(treeiter,0)[0] - else: - return None - - def __get_selected_surface(self): - surf_id = self.__get_selected_id() - if not surf_id: - return None - else: - return self.paramd[surf_id] - - - def treev_sel_changed(self, selection): - param = self.__get_selected_surface() - treeiter = self.treev_sel.get_selected()[1] - if not treeiter: - self.props_frame.hide() - return - self.props_frame.show() - try: - self.ignore_settings_updates = True - self.scrollbar_threshold.set_value(param.intensity) - if param.colorName==self.color_chooser.custom_str: - self.color_chooser._set_color(param.color) - else: - self.color_chooser._set_color(param.colorName) - self.scrollbar_opacity.set_value(param.opacity) - self.update_pipeline_params() - is_picker_surface = param.uuid==self.picker_surface_id - self.pickerButton.set_active(is_picker_surface) - self.pickerButton.set_sensitive(not is_picker_surface) - except Exception, e: - "During reacting to treeview selection change:", type(e), e - finally: - self.ignore_settings_updates = False - - def change_threshold_of_surf(self,*args): - param = self.__get_selected_surface() - if not param: - return - param.intensity = self.scrollbar_threshold.get_value() - param.update_pipeline() - self.render() - - def change_opacity_of_surf(self,*args): - param = self.__get_selected_surface() - param.set_opacity(self.scrollbar_opacity.get_value()) - self.render() - - def change_color_of_surf(self,*args): - param = self.__get_selected_surface() - self.lastColor = self.color_chooser.color - self.lastColorName = self.color_chooser.colorName - param.set_color(self.lastColor,self.lastColorName) - self.render() - - def set_surface_for_picking(self, button, *args): - if self.ignore_settings_updates: - return - param = self.__get_selected_surface() - button.set_sensitive(False) - self.picker_surface_id = param.uuid - EventHandler().notify("set picker surface", param.uuid) - - def cb_remove(self, *args): - surf_id = self.__get_selected_id() - treestore, treeiter = self.treev_sel.get_selected() - if treeiter: - treestore.remove(treeiter) - self.paramd[surf_id].destroy() - del self.paramd[surf_id] - self.__update_treeview_visibility() - self.render() - - def cb_edit_name(self, *args): - param = self.__get_selected_surface() - new_name = edit_label(param.label, "Please enter a new name\nfor the selected surface") - if not new_name or new_name==param.label: return - param.label = new_name - treeiter = self.treev_sel.get_selected()[1] - if treeiter: - return self.tree_surf.set(treeiter,1,str(new_name)) - - def __update_treeview_visibility(self): - if self.tree_surf.get_iter_first()==None: - # tree is empty - self.emptyIndicator.show() - self.treev_surf.hide() - else: - self.emptyIndicator.hide() - self.treev_surf.show() - - def set_image_data(self, data): - self.imageData = data - - def update_viewer(self, event, *args): - if event=='set image data': - self.set_image_data(args[0]) diff --git a/pylocator/tests/__init__.py b/pylocator/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pylocator/tests/test_gui.py b/pylocator/tests/test_gui.py deleted file mode 100644 index 377ae51..0000000 --- a/pylocator/tests/test_gui.py +++ /dev/null @@ -1,26 +0,0 @@ -import gobject, gtk -import getopt,sys,os - -from pylocator.controller import PyLocatorController -from pylocator.shared import shared - -from nose.tools import assert_raises - -def test_mainwindow_noargs(): - controller = PyLocatorController() - assert controller.window is not None - -#def test_mainwindow(): -# my_argv = ["-f", "somestrangeand_inexistent_file.nii.gz"] -# options, args = getopt.getopt(my_argv[:], 'hf:s:', ['help','filename','surface']) -# filename=None -# surface=None -# for option, value in options: -# if option in ('-h', '--help'): -# print usage; sys.exit(0) -# if option in ('-f', '--file'): -# filename=value -# if option in ('-s', '--surface'): -# surface=value -# -# assert_raises(IOError,PyLocatorMainWindow,filename=filename,surface=surface) diff --git a/pylocator/vtkNifti.py b/pylocator/vtkNifti.py deleted file mode 100644 index 478fae2..0000000 --- a/pylocator/vtkNifti.py +++ /dev/null @@ -1,144 +0,0 @@ -from nibabel import load -#from numpy import oldnumeric as Numeric -import numpy as np -import vtk -from shared import shared -from vtkutils import array_to_vtkmatrix4x4 - -#from vtk.util.vtkImageImportFromArray import vtkImageImportFromArray - -class vtkNiftiImageReader(object): - __defaultFilePattern="" - - def __init__(self): - self.__vtkimport=vtk.vtkImageImport() - self.__vtkimport.SetDataScalarTypeToFloat() - self.__vtkimport.SetNumberOfScalarComponents(1) - self.__filePattern=self.__defaultFilePattern - self.__data = None - self._irs = vtk.vtkImageReslice() - - def SetFileName(self, filename): - self.__filename=filename - - def Update(self): - if shared.debug: print "Loading ", self.__filename - self.__nim=load(self.__filename) - if shared.debug: print self.__nim - self.__data=self.__nim.get_data().astype("f").swapaxes(0,2) - #self.__vtkimport.SetDataExtent(0,self.__data.shape[2]-1,0,self.__data.shape[1]-1,0,self.__data.shape[0]-1) - self.__vtkimport.SetWholeExtent(0,self.__data.shape[2]-1,0,self.__data.shape[1]-1,0,self.__data.shape[0]-1) - self.__vtkimport.SetDataExtentToWholeExtent() - voxdim = self.__nim.get_header()['pixdim'][:3].copy() - #Export data as string - self.__data_string = self.__data.tostring() - if shared.debug: print voxdim - self.__vtkimport.SetDataSpacing((1.,1.,1.))#to reverse: [::-1] - self.__vtkimport.CopyImportVoidPointer(self.__data_string,len(self.__data_string)) - self.__vtkimport.UpdateWholeExtent() - - imgData1 = self.__vtkimport.GetOutput() - imgData1.SetExtent(self.__vtkimport.GetDataExtent()) - imgData1.SetOrigin((0,0,0)) - imgData1.SetSpacing(1.,1.,1.) - #print imgData1 - #print self._irs - #self._irs.SetInputConnection(self.__vtkimport.GetOutputPort()) - self._irs.SetInput(imgData1) - #print self._irs - self._irs.SetInterpolationModeToCubic() - #self._irs.AutoCropOutputOn() - - - affine = array_to_vtkmatrix4x4(self.__nim.get_affine()) - if shared.debug: print self._irs.GetResliceAxesOrigin() - self._irs.SetResliceAxes(affine) - if shared.debug: print self._irs.GetResliceAxesOrigin() - m2t = vtk.vtkMatrixToLinearTransform() - m2t.SetInput(affine.Invert()) - self._irs.TransformInputSamplingOff() - self._irs.SetResliceTransform(m2t.MakeTransform()) - #self._irs.SetResliceAxesOrigin((0.,0.,0.)) #self.__vtkimport.GetOutput().GetOrigin()) - #print self.__vtkimport.GetOutput().GetBounds() - #print self._irs.GetOutput().GetBounds() - - if shared.debug: print voxdim, self._irs.GetOutputSpacing() - self._irs.SetOutputSpacing(abs(voxdim)) - if shared.debug: print self._irs.GetOutputSpacing() - #print self._irs.GetOutputOrigin() - #self._irs.SetOutputOrigin((0,0,0)) - # print self._irs.GetOutputOrigin() - - #m2t_i.DeepCopy(m2t); - #m2t_i = affine_i.Invert(); - #m2t_i.MultiplyPoint(); - #self._irs.SetOutputOrigin(self.__nim.get_affine()[:3,-1]) - - #self._irs.SetOutputExtent(self.__vtkimport.GetDataExtent()) - self._irs.AutoCropOutputOn() - - self._irs.Update() - - - def GetWidth(self): - return self._irs.GetOutput().GetBounds()[0:2] - - def GetHeight(self): - return self._irs.GetOutput().GetBounds()[2:4] - - def GetDepth(self): - return self._irs.GetOutput().GetBouds()[4:] - - def GetDataSpacing(self): - if shared.debug: print self.__spacing, "*******************" - return self._irs.GetOutput().GetSpacing() - - def GetOutput(self): - #return self.__vtkimport.GetOutput() - #imageData = self._irs.GetOutput() - #return imageData - return self._irs.GetOutput() - - def GetFilename(self): - if shared.debug: print self.__filename - return self.__filename - - def GetDataExtent(self): - return self._irs.GetOutput().GetDataExtent() - - def GetBounds(self): - return self._irs.GetOutput().GetBounds() - - def GetQForm(self): - return self.__nim.get_affine() - - @property - def nifti_voxdim(self): - return self.__nim.get_header()['pixdim'][:3] - - @property - def shape(self): - return self.__nim.shape - - @property - def min(self): - if self.__data!=None: - return self.__data.min() - - @property - def max(self): - if self.__data!=None: - return self.__data.max() - - @property - def median(self): - d = self.__data - if d!=None: - return np.median(d[d!=0]) - -if __name__ == "__main__": - reader = vtkNiftiImageReader() - reader.SetFileName("/home/thorsten/Dokumente/pylocator-examples/Can7/mri/post2std_brain.nii.gz") - reader.Update() - print reader._irs - print reader.GetOutput() diff --git a/pylocator/vtksurface.py b/pylocator/vtksurface.py deleted file mode 100644 index 0019100..0000000 --- a/pylocator/vtksurface.py +++ /dev/null @@ -1,140 +0,0 @@ -import vtk -from events import EventHandler -from vtkutils import vtkmatrix4x4_to_array, array_to_vtkmatrix4x4 - -class VTKSurface(vtk.vtkActor): - """ - CLASS: VTKSurface - DESCR: Handles a .vtk structured points file. - """ - - def set_matrix(self, registration_mat): - print "VTKSurface.set_matrix(", registration_mat, ")!!" - - #print "calling SetUserMatrix(", array_to_vtkmatrix4x4(registration_mat) , ")" - mat = array_to_vtkmatrix4x4(registration_mat) - mat.Modified() - - mat2xform = vtk.vtkMatrixToLinearTransform() - mat2xform.SetInput(mat) - - print "calling SetUserTransform(", mat2xform, ")" - self.SetUserTransform(mat2xform) # see vtk Prop3d docs - self.Modified() - # how do we like update the render tree or somethin.. - self.renderer.Render() - - def __init__(self, filename, renderer): - - self.renderer = renderer - - reader = vtk.vtkStructuredPointsReader() - #reader.SetFileName('/home/mcc/src/devel/extract_mri_slices/braintest2.vtk') - reader.SetFileName(filename) - - # we want to move this from its (.87 .92 .43) esque position to something more like 'the center' - # how to do this?!? - - # ALTERNATIVELY: we want to use vtkInteractorStyleTrackballActor - # somewhere instead of the interactor controlling the main window and 3 planes - - - imagedata = reader.GetOutput() - - #reader.SetFileName(filename) - cf = vtk.vtkContourFilter() - cf.SetInput(imagedata) - # ??? - cf.SetValue(0, 1) - - deci = vtk.vtkDecimatePro() - deci.SetInput(cf.GetOutput()) - deci.SetTargetReduction(.1) - deci.PreserveTopologyOn() - - - smoother = vtk.vtkSmoothPolyDataFilter() - smoother.SetInput(deci.GetOutput()) - smoother.SetNumberOfIterations(100) - - - - # XXX try to call SetScale directly on actor.. - #self.scaleTransform = vtk.vtkTransform() - #self.scaleTransform.Identity() - #self.scaleTransform.Scale(.1, .1, .1) - - - - #transformFilter = vtk.vtkTransformPolyDataFilter() - #transformFilter.SetTransform(self.scaleTransform) - #transformFilter.SetInput(smoother.GetOutput()) - - - #cf.SetValue(1, 2) - #cf.SetValue(2, 3) - #cf.GenerateValues(0, -1.0, 1.0) - - #deci = vtk.vtkDecimatePro() - #deci.SetInput(cf.GetOutput()) - #deci.SetTargetReduction(0.8) # decimate_value - - normals = vtk.vtkPolyDataNormals() - #normals.SetInput(transformFilter.GetOutput()) - normals.SetInput(smoother.GetOutput()) - normals.FlipNormalsOn() - - """ - tags = vtk.vtkFloatArray() - tags.InsertNextValue(1.0) - tags.InsertNextValue(0.5) - tags.InsertNextValue(0.7) - tags.SetName("tag") - """ - - lut = vtk.vtkLookupTable() - lut.SetHueRange(0, 0) - lut.SetSaturationRange(0, 0) - lut.SetValueRange(0.2, 0.55) - - - - contourMapper = vtk.vtkPolyDataMapper() - contourMapper.SetInput(normals.GetOutput()) - - contourMapper.SetLookupTable(lut) - - ###contourMapper.SetColorModeToMapScalars() - ###contourMapper.SelectColorArray("tag") - - self.contours = vtk.vtkActor() - self.contours.SetMapper(contourMapper) - #if (do_wireframe): - #self.contours.GetProperty().SetRepresentationToWireframe() - #elif (do_surface): - self.contours.GetProperty().SetRepresentationToSurface() - self.contours.GetProperty().SetInterpolationToGouraud() - self.contours.GetProperty().SetOpacity(1.0) - self.contours.GetProperty().SetAmbient(0.1) - self.contours.GetProperty().SetDiffuse(0.1) - self.contours.GetProperty().SetSpecular(0.1) - self.contours.GetProperty().SetSpecularPower(0.1) - - # XXX arbitrarily setting scale to this - #self.contours.SetScale(.1, .1,.1) - - renderer.AddActor(self.contours) - # XXX: mcc will this work?!? - - print "PlaneWidgetsXYZ.set_image_data: setting EventHandler.set_vtkactor(self.contours)!" - EventHandler().set_vtkactor(self.contours) - - #writer = vtk.vtkSTLWriter() - #writer.SetFileTypeToBinary() - #writer.SetFileName('/home/mcc/src/devel/extract_mri_slices/braintest2.stl') - #writer.SetInput(normals.GetOutput()) - #writer.Write() - ###################################################################### - ###################################################################### - ###################################################################### - diff --git a/pylocator/vtkutils.py b/pylocator/vtkutils.py deleted file mode 100644 index 01fba57..0000000 --- a/pylocator/vtkutils.py +++ /dev/null @@ -1,32 +0,0 @@ -import vtk -import numpy as np - -def vtkmatrix4x4_to_array(vtkmat): - numpy_array = np.zeros((4,4), 'd') - for i in range(0,4): - for j in range(0,4): - numpy_array[i,j] = vtkmat.GetElement(i,j) - return numpy_array - -def array_to_vtkmatrix4x4(numpy_array): - mat = vtk.vtkMatrix4x4() - for i in range(0,4): - for j in range(0,4): - mat.SetElement(i,j, numpy_array[i,j]) - return mat - -def create_box_actor_around_marker(marker): - boxSource = create_box_source(marker) - mapper = vtk.vtkPolyDataMapper() - mapper.SetInput(boxSource.GetOutput()) - actor = vtk.vtkActor() - actor.SetMapper(mapper) - actor.GetProperty().SetColor( marker.get_color() ) - actor.GetProperty().SetRepresentationToWireframe() - actor.GetProperty().SetLineWidth(2.0) - return actor - -def create_box_source(marker): - boxSource = vtk.vtkCubeSource() - boxSource.SetBounds(marker.GetBounds()) - return boxSource diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f86b2dc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +numpy>=1.26 +nibabel>=5.2 +vtk>=9.3 +PySide6>=6.6 diff --git a/setup.cfg b/setup.cfg index e434618..c83250c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,29 +1,17 @@ [aliases] release = egg_info -RDb '' -# Make sure the sphinx docs are built each time we do a dist. bdist = build_sphinx bdist sdist = build_sphinx sdist -# Make sure a zip file is created each time we build the sphinx docs -#build_sphinx = build_sphinx zip_help [build_sphinx] source-dir = doc/src build-dir = doc/build all_files = 1 -[egg_info] -tag_build = .dev - -[bdist_rpm] -doc-files = doc - -[nosetests] -verbosity = 2 -detailed-errors = 1 -with-coverage = 1 -cover-package = pylocator -#pdb = 1 -#pdb-failures = 1 -with-doctest=1 -doctest-extension=rst - +[tool:pytest] +addopts = -ra +filterwarnings = + ignore::DeprecationWarning + ignore::FutureWarning +testpaths = + tests diff --git a/setup.py b/setup.py index d9eb8c3..415799d 100644 --- a/setup.py +++ b/setup.py @@ -1,67 +1,33 @@ -#!/usr/bin/env python - -from distutils.core import setup -import sys - -import pylocator - - -# For some commands, use setuptools -if len(set(('develop', 'sdist', 'release', 'bdist_egg', 'bdist_rpm', - 'bdist', 'bdist_dumb', 'bdist_wininst', 'install_egg_info', - 'build_sphinx', 'egg_info', 'easy_install', - )).intersection(sys.argv)) > 0: - from setupegg import extra_setuptools_args - -# extra_setuptools_args is injected by the setupegg.py script, for -# running the setup with setuptools. -if not 'extra_setuptools_args' in globals(): - extra_setuptools_args = dict() - - -setup(name='pylocator', - version=pylocator.__version__, - summary='Program for the localization of EEG-electrodes.', - author='Thorsten Kranz', - author_email='thorstenkranz@gmail.com', - url='http://pylocator.thorstenkranz.de', - description=""" -Program for the localization of EEG-electrodes. -""", - long_description=file('README').read(), - license='BSD', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Intended Audience :: Education', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Scientific/Engineering', - 'Topic :: Utilities', - ], - platforms='any', - package_data={'pylocator': [ - 'resources/mainWindow.glade', - 'resources/pylocator.ico', - 'resources/markers.png', - 'resources/surfaces.png', - 'resources/rois.png', - 'resources/screenshots.png', - 'resources/editLabel.glade', - 'resources/editCoordinates.glade', - 'resources/editSettings.glade', - 'resources/about.glade', - 'resources/camera24.png', - 'resources/camera48.png', - ],}, - packages=[ - 'pylocator', - 'pylocator.misc', - 'pylocator.tests', - ], - scripts=['bin/pylocator'], - **extra_setuptools_args) - +#!/usr/bin/env python3 + +from pathlib import Path +from setuptools import find_packages, setup + +PROJECT_ROOT = Path(__file__).parent.resolve() +README = (PROJECT_ROOT / "README").read_text(encoding="utf-8") + +about: dict = {} +with open(PROJECT_ROOT / "pylocator" / "__init__.py", encoding="utf-8") as f: + exec(f.read(), about) + +setup( + name="pylocator", + version=about.get("__version__", "0.0.0"), + description="Program for the localization of EEG-electrodes.", + long_description=README, + long_description_content_type="text/plain", + author="Thorsten Kranz", + author_email="thorstenkranz@gmail.com", + url="http://pylocator.thorstenkranz.de", + license="BSD-2-Clause", + packages=find_packages(), + include_package_data=True, + python_requires=">=3.11", + install_requires=[ + "numpy>=1.26", + "nibabel>=5.2", + "vtk>=9.3", + "PySide6>=6.6", + ], + scripts=["bin/pylocator"], +) diff --git a/setupegg.py b/setupegg.py index 201cf75..9bbb75e 100755 --- a/setupegg.py +++ b/setupegg.py @@ -28,7 +28,7 @@ class ZipHelp(Command): def run(self): if not os.path.exists(DOC_BUILD_DIR): - raise OSError, 'Doc directory does not exist.' + raise OSError('Doc directory does not exist.') target_file = os.path.join('doc', 'documentation.zip') # ZIP_DEFLATED actually compresses the archive. However, there # will be a RuntimeError if zlib is not installed, so we check @@ -67,7 +67,7 @@ def finalize_options(self): if __name__ == '__main__': - execfile('setup.py', dict(__name__='__main__', + exec(compile(open('setup.py', "rb").read(), 'setup.py', 'exec'), dict(__name__='__main__', extra_setuptools_args=extra_setuptools_args)) diff --git a/tests/test_nifti_loader.py b/tests/test_nifti_loader.py new file mode 100644 index 0000000..8366ded --- /dev/null +++ b/tests/test_nifti_loader.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import nibabel as nib +import numpy as np +import pytest + +from pylocator.nifti_loader import NiftiLoadError, load_nifti_volume + + +def test_load_nifti_volume(tmp_path): + data = np.arange(27, dtype=np.float32).reshape((3, 3, 3)) + affine = np.diag([2.0, 2.0, 2.0, 1.0]).astype(np.float32) + img = nib.Nifti1Image(data, affine) + path = tmp_path / "volume.nii.gz" + nib.save(img, path) + + volume = load_nifti_volume(str(path)) + + assert volume.shape == (3, 3, 3) + assert volume.voxel_size == pytest.approx((2.0, 2.0, 2.0)) + assert volume.value_range == pytest.approx((0.0, float(data.max()))) + assert volume.image_data.GetDimensions() == (3, 3, 3) + + +def test_missing_file(tmp_path): + missing = tmp_path / "missing.nii.gz" + with pytest.raises(NiftiLoadError): + load_nifti_volume(str(missing)) + + +def test_rejects_non_3d(tmp_path): + data = np.zeros((3, 3, 3, 2), dtype=np.float32) + affine = np.eye(4, dtype=np.float32) + img = nib.Nifti1Image(data, affine) + path = tmp_path / "4d.nii.gz" + nib.save(img, path) + + with pytest.raises(NiftiLoadError): + load_nifti_volume(str(path))