From 199eed724379cb783efcd9348991c5362530a66b Mon Sep 17 00:00:00 2001 From: thorstenkranz Date: Sun, 31 Aug 2025 12:37:09 +0200 Subject: [PATCH 1/7] Port to Python 3 and add Poetry config --- bin/pylocator | 6 +-- doc/src/conf.py | 8 ++-- doc/src/sphinxext/autosummary.py | 6 +-- doc/src/sphinxext/autosummary_generate.py | 14 +++--- doc/src/sphinxext/comment_eater.py | 6 +-- doc/src/sphinxext/compiler_unparse.py | 8 ++-- doc/src/sphinxext/docscrape.py | 20 ++++----- doc/src/sphinxext/docscrape_sphinx.py | 4 +- doc/src/sphinxext/numpydoc.py | 6 +-- doc/src/sphinxext/phantom_import.py | 4 +- doc/src/sphinxext/plot_directive.py | 14 +++--- doc/src/sphinxext/traitsdoc.py | 10 ++--- .../GtkGLExtVTKRenderWindowInteractor.py | 8 ++-- pylocator/colors.py | 8 ++-- pylocator/connect_filter.py | 6 +-- pylocator/controller.py | 34 +++++++------- pylocator/decimate_filter.py | 4 +- pylocator/dialogs.py | 18 ++++---- pylocator/events.py | 28 ++++++------ pylocator/gtkutils.py | 30 ++++++------- pylocator/main.py | 4 +- pylocator/marker_list.py | 18 ++++---- pylocator/marker_window_interactor.py | 42 +++++++++--------- pylocator/markers.py | 6 +-- pylocator/misc/markers_io.py | 4 +- pylocator/plane_widgets_observer.py | 30 ++++++------- pylocator/plane_widgets_observer_toolbar.py | 4 +- pylocator/plane_widgets_xyz.py | 44 +++++++++---------- pylocator/render_window.py | 18 ++++---- pylocator/roi_renderer_props.py | 26 +++++------ pylocator/rois.py | 6 +-- pylocator/screenshot_props.py | 6 +-- pylocator/surf_params.py | 12 ++--- pylocator/surf_renderer.py | 24 +++++----- pylocator/surf_renderer_props.py | 28 ++++++------ pylocator/vtkNifti.py | 26 +++++------ pylocator/vtksurface.py | 10 ++--- pyproject.toml | 20 +++++++++ setupegg.py | 4 +- 39 files changed, 297 insertions(+), 277 deletions(-) create mode 100644 pyproject.toml 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/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 index 01f73dd..04f62bc 100644 --- a/pylocator/GtkGLExtVTKRenderWindowInteractor.py +++ b/pylocator/GtkGLExtVTKRenderWindowInteractor.py @@ -14,7 +14,7 @@ import gtk.gtkgl import vtk -from shared import shared +from .shared import shared class GtkGLExtVTKRenderWindowInteractor(gtk.gtkgl.DrawingArea): """ @@ -86,8 +86,8 @@ def __getattr__(self, attr): elif hasattr(self._Iren, attr): return getattr(self._Iren, attr) else: - raise AttributeError, self.__class__.__name__ + \ - " has no attribute named " + attr + raise AttributeError(self.__class__.__name__ + \ + " has no attribute named " + attr) def CreateTimer(self, obj, event): gtk.timeout_add(10, self._Iren.TimerEvent) @@ -143,7 +143,7 @@ def _GetCtrlShift(self, event): return ctrl, shift def OnButtonDown(self, wid, event): - if shared.debug: print "GtkGLExtVTKRenderWindowInteractor.OnButtonDown()" + if shared.debug: print("GtkGLExtVTKRenderWindowInteractor.OnButtonDown()") """Mouse button pressed.""" m = self.get_pointer() ctrl, shift = self._GetCtrlShift(event) diff --git a/pylocator/colors.py b/pylocator/colors.py index 4ba03c2..0ee6b5e 100644 --- a/pylocator/colors.py +++ b/pylocator/colors.py @@ -1,9 +1,9 @@ -from __future__ import division + import gobject import gtk -from gtkutils import make_option_menu +from .gtkutils import make_option_menu colorSeq = ( ( 'light skin' , (0.953, 0.875, 0.765) ), @@ -74,7 +74,7 @@ def choose_color(self, *args): class ColorChooserWithPredefinedColors(gtk.HBox): custom_str = "custom..." def __init__(self, colorSeq=colorSeq): - names, self.colors= zip(*colorSeq) + names, self.colors= list(zip(*colorSeq)) self.colorDict = dict(colorSeq) self.colorNames = list(names) @@ -181,7 +181,7 @@ def gdkColor2tuple(color): if __name__=="__main__": def func(cc): - print "Color changed", cc.get_color() + print("Color changed", cc.get_color()) win = gtk.Window() box = gtk.VBox() diff --git a/pylocator/connect_filter.py b/pylocator/connect_filter.py index 47a1ba2..87eaf3d 100644 --- a/pylocator/connect_filter.py +++ b/pylocator/connect_filter.py @@ -1,7 +1,7 @@ -from __future__ import division + import vtk import gtk -from gtkutils import ProgressBarDialog +from .gtkutils import ProgressBarDialog class ConnectFilter(vtk.vtkPolyDataConnectivityFilter): """ @@ -18,7 +18,7 @@ class ConnectFilter(vtk.vtkPolyDataConnectivityFilter): 'All Regions' : 5, 'Closest Point Region' : 6, } - num2mode = dict([ (v,k) for k,v in mode2num.items()]) + num2mode = dict([ (v,k) for k,v in list(mode2num.items())]) mode = 5 def __init__(self): diff --git a/pylocator/controller.py b/pylocator/controller.py index aead074..42d87a5 100644 --- a/pylocator/controller.py +++ b/pylocator/controller.py @@ -1,26 +1,26 @@ import os import gtk -from shared import shared -from events import EventHandler -from vtkNifti import vtkNiftiImageReader +from .shared import shared +from .events import EventHandler +from .vtkNifti import vtkNiftiImageReader -from surf_renderer import SurfRenderWindow -from plane_widgets_xyz import PlaneWidgetsXYZ, move_pw_to_point -from plane_widgets_observer import PlaneWidgetObserver -from plane_widgets_observer_toolbar import ObserverToolbar +from .surf_renderer import SurfRenderWindow +from .plane_widgets_xyz import PlaneWidgetsXYZ, move_pw_to_point +from .plane_widgets_observer import PlaneWidgetObserver +from .plane_widgets_observer_toolbar import ObserverToolbar -from surf_renderer_props import SurfRendererProps -from roi_renderer_props import RoiRendererProps -from screenshot_props import ScreenshotProps +from .surf_renderer_props import SurfRendererProps +from .roi_renderer_props import RoiRendererProps +from .screenshot_props import ScreenshotProps -from marker_list import MarkerList -from gtkutils import simple_msg +from .marker_list import MarkerList +from .gtkutils import simple_msg import pylocator -from resources import main_window -from dialogs import SettingsController, about +from .resources import main_window +from .dialogs import SettingsController, about -from colors import gdkColor2tuple +from .colors import gdkColor2tuple class PyLocatorController(object): def __init__(self): @@ -81,7 +81,7 @@ def __fill_notebook_pages(self, window, vboxes): def __add_observer_widgets_to_window(self,win,hbox): win.observers = [] - for orientation, pw in zip(range(3),win.pwxyz.get_plane_widgets_xyz()): + for orientation, pw in zip(list(range(3)),win.pwxyz.get_plane_widgets_xyz()): vboxObs = gtk.VBox() vboxObs.show() observer = PlaneWidgetObserver(pw, owner=win, orientation=orientation) @@ -176,7 +176,7 @@ def load_nifti(self, filename): filename = dialog.get_filename() dialog.destroy() if response == gtk.RESPONSE_OK: - print "Loading:", filename + print("Loading:", filename) else: return False diff --git a/pylocator/decimate_filter.py b/pylocator/decimate_filter.py index 75e1209..16b2e59 100644 --- a/pylocator/decimate_filter.py +++ b/pylocator/decimate_filter.py @@ -1,10 +1,10 @@ -from __future__ import division + import sys, os import vtk import gtk -from gtkutils import ProgressBarDialog, str2posnum_or_err +from .gtkutils import ProgressBarDialog, str2posnum_or_err # vtkDecimate is patented and no longer in VTK5. we will try vtkDecimatePro (argh) diff --git a/pylocator/dialogs.py b/pylocator/dialogs.py index 5e0342b..8b405f2 100644 --- a/pylocator/dialogs.py +++ b/pylocator/dialogs.py @@ -1,10 +1,10 @@ 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 +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): @@ -30,16 +30,16 @@ def edit_label(oldLabel="", description=None): def edit_label_of_marker(marker): label = marker.get_label() defaultLabel = label - print defaultLabel, shared.lastLabel + 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 + print(defaultLabel) new_label = edit_label(defaultLabel) - if shared.debug: print new_label, label + if shared.debug: print(new_label, label) if new_label==None or new_label==label: return EventHandler().notify('label marker', marker, new_label) @@ -147,7 +147,7 @@ def set_default_color(self, *args): EventHandler().set_default_color(gdkColor2tuple(color)) def close_dialog(self, *args): - print "close dialog" + print("close dialog") self.dialog.hide() self.dialog.destroy() diff --git a/pylocator/events.py b/pylocator/events.py index bfe7744..1d53597 100644 --- a/pylocator/events.py +++ b/pylocator/events.py @@ -1,8 +1,8 @@ import vtk -from markers import Marker +from .markers import Marker import pickle -from shared import shared -from vtkutils import vtkmatrix4x4_to_array +from .shared import shared +from .vtkutils import vtkmatrix4x4_to_array class UndoRegistry: __sharedState = {} @@ -50,12 +50,12 @@ def add_selection(self, marker): self.notify('select marker', marker) def remove_selection(self, marker): - if self.selected.has_key(marker): + if marker in self.selected: del self.selected[marker] self.notify('unselect marker', marker) def clear_selection(self): - for oldMarker in self.selected.keys(): + for oldMarker in list(self.selected.keys()): self.remove_selection(oldMarker) def select_new(self, marker): @@ -130,11 +130,11 @@ def get_nifti_stats(self): self.__NiftiMax) def set_vtkactor(self, vtkactor): - if shared.debug: print "EventHandler.set_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,")" + if shared.debug: print("EventHandler.save_registration_as(", fname,")") fh = file(fname, 'w') # XXX mcc: somehow get the transform for the VTK actor. aiieeee @@ -145,7 +145,7 @@ def save_registration_as(self, fname): 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, "!!" + 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) @@ -170,13 +170,13 @@ def detach(self, observer): except KeyError: pass def notify(self, event, *args): - for observer in self.observers.keys(): + for observer in list(self.observers.keys()): if shared.debug: - print "EventHandler.notify(", event, "): calling update_viewer for ", observer + 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 + except Exception as e: + print("Error while updating observer", observer, type(e), e) def get_labels_on(self): return self.labelsOn @@ -190,10 +190,10 @@ def set_labels_off(self): self.notify('labels off') def is_selected(self, marker): - return self.selected.has_key(marker) + return marker in self.selected def get_selected(self): - return self.selected.keys() + return list(self.selected.keys()) def get_num_selected(self): return len(self.selected) diff --git a/pylocator/gtkutils.py b/pylocator/gtkutils.py index 27ed038..1b36494 100644 --- a/pylocator/gtkutils.py +++ b/pylocator/gtkutils.py @@ -1,9 +1,9 @@ import os, sys -import StringIO, traceback +import io, traceback import gobject, gtk from gtk import gdk -from shared import shared +from .shared import shared import datetime def is_string_like(obj): @@ -15,8 +15,8 @@ def is_string_like(obj): def exception_to_str(s = None): - sh = StringIO.StringIO() - if s is not None: print >>sh, s + sh = io.StringIO() + if s is not None: print(s, file=sh) traceback.print_exc(file=sh) return sh.getvalue() @@ -153,7 +153,7 @@ def __init__(self, defaultDir, okCallback, title='Select file', """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__" + if shared.debug: print("Dialog_FileSelection.__init__") self.defaultDir = defaultDir self.okCallback = okCallback gtk.FileSelection.__init__(self, title=title) @@ -391,22 +391,22 @@ def get_num_range(minLabel='Min', maxLabel='Max', 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(':')) + x= list(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) + 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(':')) + x= list(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) + 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 @@ -454,7 +454,7 @@ def select_name(names, title='Select Name'): response = dlg.run() if response == gtk.RESPONSE_OK: - for button, name in buttond.items(): + for button, name in list(buttond.items()): if button.get_active(): dlg.destroy() return name @@ -616,12 +616,12 @@ def make_float_string(value): try: rv = "%.2f"%float(value) return rv - except Exception, e: + except Exception as e: return str(value) dlg = gtk.Dialog(title) if parent is not None: - print "parent not None:", parent + print("parent not None:", parent) dlg.set_transient_for(parent) vbox = dlg.vbox @@ -764,7 +764,7 @@ 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: + except IOError as msg: msg = exception_to_str('Could not open %s' % filename) error_msg(msg, parent=self.parentWin) else: @@ -789,7 +789,7 @@ def save(self, button, **kwargs): try: outfile = file(filename, 'w') - except IOError, msg: + except IOError as msg: msg = exception_to_str('Could not write markers to %s' % filename) error_msg(msg, parent=self.parentWin) return @@ -919,7 +919,7 @@ def save(self, *args): # todo: add csv extension fh = file(filename, 'w', False) for row in self.rows: - print >>fh, ','.join(row) + print(','.join(row), file=fh) fh.close() diff --git a/pylocator/main.py b/pylocator/main.py index a5bddc9..3c43645 100644 --- a/pylocator/main.py +++ b/pylocator/main.py @@ -2,8 +2,8 @@ import gtk import os.path -from controller import PyLocatorController -from shared import shared +from .controller import PyLocatorController +from .shared import shared def run_pylocator(filename=None, surface=None): """main method to run when PyLocator is started""" diff --git a/pylocator/marker_list.py b/pylocator/marker_list.py index 0e02a3a..c1835d2 100644 --- a/pylocator/marker_list.py +++ b/pylocator/marker_list.py @@ -1,14 +1,14 @@ -from __future__ import division + import gobject import gtk -from dialogs import edit_coordinates, edit_label_of_marker +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 +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): @@ -238,8 +238,8 @@ def remove_marker(self,marker): self.tree_mrk.remove(treeiter) del self._markers[id_] del self._marker_ids[marker.uuid] - except Exception, e: - print "Exception in MarkerList.remove_marker" + except Exception as e: + print("Exception in MarkerList.remove_marker") finally: self.__update_treeview_visibility() diff --git a/pylocator/marker_window_interactor.py b/pylocator/marker_window_interactor.py index 37fef62..ac343b2 100644 --- a/pylocator/marker_window_interactor.py +++ b/pylocator/marker_window_interactor.py @@ -1,9 +1,9 @@ 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 +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 @@ -42,25 +42,25 @@ def 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()" + 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()" + 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()" + 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()" + 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()" + 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()" + 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()" + if shared.debug: print("MarkerWindowInteractor.set_mouse1_to_move()") self.set_mouse1_to_move() def get_marker_at_point(self): @@ -73,11 +73,11 @@ def set_select_mode(self): pass def set_interact_mode(self): - if shared.debug: print "set_interact_mode()!!!!" + 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 shared.debug: print("set_vtkinteract_mode()!!!!") if (self.vtk_interact_mode == False): # mcc XXX: ignore this @@ -87,7 +87,7 @@ def set_vtkinteract_mode(self): def set_mouse1_to_interact(self): - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_interact()" + if shared.debug: print("MarkerWindowInteractor.set_mouse1_to_interact()") self.vtk_interact_mode = False @@ -105,12 +105,12 @@ def set_mouse1_to_interact(self): self.window.set_cursor (cursor) def vtkinteraction_event(self, *args): - if shared.debug: print "vtkinteraction_event!!!" + if shared.debug: print("vtkinteraction_event!!!") self.Render() def set_mouse1_to_vtkinteract(self): - if shared.debug: print "MarkerWindowInteractor.set_mouse1_to_vtkinteract()" + if shared.debug: print("MarkerWindowInteractor.set_mouse1_to_vtkinteract()") self.set_vtkinteract_mode() @@ -228,14 +228,14 @@ def OnButtonDown(self, wid, event): 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 + 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 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 shared.debug: print("self.vtk_interact_mode =", self.vtk_interact_mode) if (self.vtk_interact_mode == False): self.pressFuncs[event.button]() @@ -246,13 +246,13 @@ 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 + 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 shared.debug: print("self.vtk_interact_mode =", self.vtk_interact_mode) if (self.vtk_interact_mode == False): self.releaseFuncs[event.button]() diff --git a/pylocator/markers.py b/pylocator/markers.py index 9de6baa..630c9f7 100644 --- a/pylocator/markers.py +++ b/pylocator/markers.py @@ -6,7 +6,7 @@ import numpy as n -from shared import shared +from .shared import shared class Marker(vtk.vtkActor): """ @@ -86,7 +86,7 @@ def set_size(self, s): return self.sphere.SetRadius(s) def set_color(self, color): - if shared.debug: print "Marker.GetProperty().SetColor(", color, ")" + if shared.debug: print("Marker.GetProperty().SetColor(", color, ")") self.GetProperty().SetColor( color ) def get_color(self): @@ -112,7 +112,7 @@ 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:]) + x,y,z,radius,r,g,b = list(map(float, vals[1:])) marker = Marker(xyz=(x,y,z), radius=radius, rgb=(r,g,b)) marker.set_label(label) return marker diff --git a/pylocator/misc/markers_io.py b/pylocator/misc/markers_io.py index 6b42ac9..7b533d6 100644 --- a/pylocator/misc/markers_io.py +++ b/pylocator/misc/markers_io.py @@ -25,7 +25,7 @@ def load_markers_to_dict(fh): if __name__=="__main__": fn = "/media/Extern/public/Experimente/AudioStroop/kombinierte_analyse/elec_pos/443.txt" - print load_markers(fn) + print(load_markers(fn)) fh = open(fn,"r") - print load_markers(fh) + print(load_markers(fh)) diff --git a/pylocator/plane_widgets_observer.py b/pylocator/plane_widgets_observer.py index b6491b2..c32810a 100644 --- a/pylocator/plane_widgets_observer.py +++ b/pylocator/plane_widgets_observer.py @@ -2,14 +2,14 @@ import gtk import vtk import time -from markers import Marker, RingActor -from events import EventHandler, UndoRegistry +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 +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 @@ -21,7 +21,7 @@ class PlaneWidgetObserver(MarkerWindowInteractor): axes_labels_color = (0.,0.82,1.) def __init__(self, planeWidget, owner, orientation, imageData=None): - if shared.debug: print "PlaneWidgetObserver.__init__(): orientation=",orientation + if shared.debug: print("PlaneWidgetObserver.__init__(): orientation=",orientation) MarkerWindowInteractor.__init__(self) self.interactButtons = (1,2,3) self.pw = planeWidget @@ -44,9 +44,9 @@ 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)" + 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 + if shared.debug: print("PlaneWidgetObserver.set_image_data(): AddObserver call returns ", foo) self.connect("scroll_event", self.scroll_widget_slice) self.hasData = 1 @@ -88,7 +88,7 @@ def add_axes_labels(self): 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 shared.debug: print(i,b, coords, label) if self.orientation == 0: if label in ["R","L"]: continue @@ -118,7 +118,7 @@ def add_axes_labels(self): center = self.imageData.GetCenter() spacing = self.imageData.GetSpacing() bounds = np.array(self.imageData.GetBounds()) - if shared.debug: print "***center,spacing,bounds", center,spacing,bounds + 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: @@ -130,9 +130,9 @@ def add_axes_labels(self): elif self.orientation == 2: pos[2] += max((bounds[1::2]-bounds[0::2]))*2 camera_up[0] = -1 - if shared.debug: print camera_up + if shared.debug: print(camera_up) fpu = center, pos, tuple(camera_up) - if shared.debug: print "***fpu2:", fpu + if shared.debug: print("***fpu2:", fpu) self.set_camera(fpu) self.scroll_depth(self.sliceIncrement) @@ -357,7 +357,7 @@ def update_rings(self): textActor.VisibilityOff() def update_rois(self): - for actor in self.roi_actors.values(): + for actor in list(self.roi_actors.values()): actor.update() def interaction_event(self, *args): @@ -486,12 +486,12 @@ def label_ring_actor(self, marker, label): textActor.SetMapper(textMapper) def get_actor_for_marker(self, marker): - if self.ringActors.has_key(marker.uuid): + if marker.uuid in self.ringActors: return self.ringActors[marker.uuid] return None def get_ring_actors_as_list(self): - return self.ringActors.values() + return list(self.ringActors.values()) def get_cursor_position_world(self): x, y = self.GetEventPosition() diff --git a/pylocator/plane_widgets_observer_toolbar.py b/pylocator/plane_widgets_observer_toolbar.py index 381ce2f..b2db323 100644 --- a/pylocator/plane_widgets_observer_toolbar.py +++ b/pylocator/plane_widgets_observer_toolbar.py @@ -1,7 +1,7 @@ import gtk -from gtkutils import error_msg +from .gtkutils import error_msg import vtk -from events import EventHandler +from .events import EventHandler def move_pw_to_point(pw, xyz): diff --git a/pylocator/plane_widgets_xyz.py b/pylocator/plane_widgets_xyz.py index 2718764..71930ea 100644 --- a/pylocator/plane_widgets_xyz.py +++ b/pylocator/plane_widgets_xyz.py @@ -1,11 +1,11 @@ import vtk -from events import EventHandler, UndoRegistry -from render_window import ThreeDimRenderWindow -from marker_window_interactor import MarkerWindowInteractor +from .events import EventHandler, UndoRegistry +from .render_window import ThreeDimRenderWindow +from .marker_window_interactor import MarkerWindowInteractor import numpy as np -from shared import shared +from .shared import shared def move_pw_to_point(pw, xyz): @@ -38,7 +38,7 @@ def __init__(self, imageData=None): MarkerWindowInteractor.__init__(self) ThreeDimRenderWindow.__init__(self) - if shared.debug: print "PlaneWidgetsXYZ.__init__()" + if shared.debug: print("PlaneWidgetsXYZ.__init__()") self.vtksurface = None @@ -86,11 +86,11 @@ def rotate_vtk(self, axis, value): self.Render() def set_image_data(self, imageData): - if shared.debug: print "PlaneWidgetsXYZ.set_image_data()!!" + 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 + if shared.debug: print("***Extent:", extent) frac = 0.3 self._plane_widget_boilerplate( @@ -133,7 +133,7 @@ def add_axes_labels(self): #Correction for negative spacings idx_label = 1*i label = labels[idx_label] - if shared.debug: print i,b, coords, 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) @@ -153,16 +153,16 @@ def add_axes_labels(self): center = self.imageData.GetCenter() spacing = self.imageData.GetSpacing() bounds = np.array(self.imageData.GetBounds()) - if shared.debug: print "***center,spacing,bounds", center,spacing,bounds + 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 + if shared.debug: print(camera_up) fpu = center, pos, tuple(camera_up) - if shared.debug: print "***fpu2:", fpu + if shared.debug: print("***fpu2:", fpu) self.set_camera(fpu) def get_marker_at_point(self): @@ -182,7 +182,7 @@ def update_viewer(self, event, *args): marker, label = args marker.set_label(label) - if shared.debug: print "Create VTK-Text", marker.get_label() + if shared.debug: print("Create VTK-Text", marker.get_label()) text = vtk.vtkVectorText() text.SetText(marker.get_label()) textMapper = vtk.vtkPolyDataMapper() @@ -200,7 +200,7 @@ def update_viewer(self, event, *args): x,y,z = marker.get_center() textActor.SetPosition(x+size, y+size, z+size) - if self.boxes.has_key(marker): + if marker in self.boxes: selectActor = self.boxes[marker] boxSource = vtk.vtkCubeSource() boxSource.SetBounds(marker.GetBounds()) @@ -209,11 +209,11 @@ def update_viewer(self, event, *args): selectActor.SetMapper(mapper) elif event=='labels on': - actors = self.textActors.values() + actors = list(self.textActors.values()) for actor in actors: actor.VisibilityOn() elif event=='labels off': - actors = self.textActors.values() + actors = list(self.textActors.values()) for actor in actors: actor.VisibilityOff() #elif event=='select marker': @@ -238,18 +238,18 @@ def update_viewer(self, event, *args): def _plane_widget_boilerplate(self, pw, key, color, index, orientation): - if shared.debug: print "PlaneWidgetsXYZ._plane_widget_boilerplate(", 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)" + 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)" + if shared.debug: print("pw " , orientation, ".SetInteractor(self.interactor)") pw.SetInteractor(self.interactor) pw.On() pw.UpdatePlacement() @@ -299,22 +299,22 @@ def set_interact_mode(self): def OnButtonDown(self, wid, event): """Mouse button pressed.""" - if shared.debug: print "PlaneWidgetsXYZ.OnButtonDown(): event=", event + 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 + if shared.debug: print("PlaneWidgetsXYZ.OnButtonDown(): self.lastPntsXYZ=", self.lastPntsXYZ) MarkerWindowInteractor.OnButtonDown(self, wid, event) - if shared.debug: print self.axes_labels + 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 shared.debug: print("PlaneWidgetsXYZ.OnButtonUp(): event=", event) if not hasattr(self, 'lastPntsXYZ'): return MarkerWindowInteractor.OnButtonUp(self, wid, event) diff --git a/pylocator/render_window.py b/pylocator/render_window.py index b9ba9f7..123e983 100644 --- a/pylocator/render_window.py +++ b/pylocator/render_window.py @@ -1,11 +1,11 @@ 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 .GtkGLExtVTKRenderWindowInteractor import GtkGLExtVTKRenderWindowInteractor +from .events import EventHandler +from .gtkutils import error_msg +from .vtkutils import create_box_actor_around_marker -from shared import shared +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 @@ -127,7 +127,7 @@ def change_roi_opacity(self, uuid, opactiy): pass def _get_roi_actor(self, uuid): - if not self.roi_actors.has_key(uuid): + if uuid not in self.roi_actors: return return self.roi_actors[uuid] @@ -208,10 +208,10 @@ def add_marker(self, marker): textActor.SetCamera(self.camera) textActor.GetProperty().SetColor(marker.get_label_color()) if EventHandler().get_labels_on(): - if shared.debug: print "VisibilityOn" + if shared.debug: print("VisibilityOn") textActor.VisibilityOn() else: - if shared.debug: print "VisibilityOff" + if shared.debug: print("VisibilityOff") textActor.VisibilityOff() self.textActors[marker] = textActor self.renderer.AddActor(textActor) @@ -227,7 +227,7 @@ def remove_marker(self, marker): 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)" + if shared.debug: print("PlaneWidgetsXYZ.update_viewer(): self.renderer.AddActor(actor)") self.renderer.AddActor(actor) self.boxes[marker] = actor else: diff --git a/pylocator/roi_renderer_props.py b/pylocator/roi_renderer_props.py index 0f1d666..96c204d 100644 --- a/pylocator/roi_renderer_props.py +++ b/pylocator/roi_renderer_props.py @@ -1,18 +1,18 @@ -from __future__ import division + import os.path import gobject import gtk -from gtkutils import error_msg, ButtonAltLabel +from .gtkutils import error_msg, ButtonAltLabel -from events import EventHandler -from shared import shared +from .events import EventHandler +from .shared import shared -from surf_params import SurfParams +from .surf_params import SurfParams -from list_toolbar import ListToolbar -from colors import ColorChooser -from vtkNifti import vtkNiftiImageReader -from rois import RoiParams +from .list_toolbar import ListToolbar +from .colors import ColorChooser +from .vtkNifti import vtkNiftiImageReader +from .rois import RoiParams class RoiRendererProps(gtk.VBox): SCROLLBARSIZE = 150,20 @@ -180,12 +180,12 @@ def treev_sel_changed(self,selection): 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 + except Exception as 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 + except Exception as e: + print("During setting value of opacity scrollbar:", type(e),e) else: self.props_frame.hide() diff --git a/pylocator/rois.py b/pylocator/rois.py index cb2726f..3178ee1 100644 --- a/pylocator/rois.py +++ b/pylocator/rois.py @@ -1,7 +1,7 @@ -from __future__ import division + import vtk -from events import EventHandler -from surf_params import SurfParams +from .events import EventHandler +from .surf_params import SurfParams class RoiParams(SurfParams): diff --git a/pylocator/screenshot_props.py b/pylocator/screenshot_props.py index b22bf76..60d87bc 100644 --- a/pylocator/screenshot_props.py +++ b/pylocator/screenshot_props.py @@ -1,7 +1,7 @@ import gtk -from gtkutils import error_msg -from resources import camera_small_fn -from shared import shared +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 diff --git a/pylocator/surf_params.py b/pylocator/surf_params.py index 6bc9bfa..4b45d98 100644 --- a/pylocator/surf_params.py +++ b/pylocator/surf_params.py @@ -1,12 +1,12 @@ -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 +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" diff --git a/pylocator/surf_renderer.py b/pylocator/surf_renderer.py index 16bb06b..2c33f05 100644 --- a/pylocator/surf_renderer.py +++ b/pylocator/surf_renderer.py @@ -1,12 +1,12 @@ -from __future__ import division + import vtk import gtk -from gtkutils import error_msg +from .gtkutils import error_msg -from events import EventHandler -from markers import Marker -from shared import shared -from render_window import PyLocatorRenderWindow, ThreeDimRenderWindow +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 @@ -30,11 +30,11 @@ def set_image_data(self, imageData): def set_labels_visibility(self, visible=True): if visible: - actors = self.textActors.values() + actors = list(self.textActors.values()) for actor in actors: actor.VisibilityOn() else: - actors = self.textActors.values() + actors = list(self.textActors.values()) for actor in actors: actor.VisibilityOff() @@ -66,7 +66,7 @@ def change_surface_opacity(self, uuid, opacity): actor.GetProperty().SetOpacity(opacity) def __get_surface_actor(self, uuid): - if not self.surface_actors.has_key(uuid): + if uuid not in self.surface_actors: return return self.surface_actors[uuid] @@ -77,7 +77,7 @@ def update_viewer(self, event, *args): self.Render() def key_press(self, interactor, event): - if shared.debug: print "key press event in SurfRenderWindow" + if shared.debug: print("key press event in SurfRenderWindow") key = interactor.GetKeySym() sas = self.surface_actors @@ -92,7 +92,7 @@ def checkPickerId(): if key.lower()=='i': if not checkPickerId(): return - if shared.debug: print "Inserting Marker" + if shared.debug: print("Inserting Marker") x,y = interactor.GetEventPosition() picker = vtk.vtkCellPicker() picker.PickFromListOn() @@ -124,6 +124,6 @@ def checkPickerId(): if cellId==-1: pass else: - o = self.paramd.values()[0] + o = list(self.paramd.values())[0] o.remove.RemoveCell(cellId) interactor.Render() diff --git a/pylocator/surf_renderer_props.py b/pylocator/surf_renderer_props.py index 265c81b..8a29660 100644 --- a/pylocator/surf_renderer_props.py +++ b/pylocator/surf_renderer_props.py @@ -1,20 +1,20 @@ -from __future__ import division + import gobject import gtk -from gtkutils import error_msg, ButtonAltLabel -from dialogs import edit_label +from .gtkutils import error_msg, ButtonAltLabel +from .dialogs import edit_label -from events import EventHandler +from .events import EventHandler -from colors import ColorChooserWithPredefinedColors, colorSeq +from .colors import ColorChooserWithPredefinedColors, colorSeq -from list_toolbar import ListToolbar -from surf_params import SurfParams +from .list_toolbar import ListToolbar +from .surf_params import SurfParams -from decimate_filter import DecimateFilter -from connect_filter import ConnectFilter +from .decimate_filter import DecimateFilter +from .connect_filter import ConnectFilter class SurfRendererProps(gtk.VBox): SCROLLBARSIZE = 150,20 @@ -198,7 +198,7 @@ def connect_toggled(button): def set_connect_mode(id_): if self.paramd[id_].useConnect: - for num in self.connectExtractButtons.keys(): + for num in list(self.connectExtractButtons.keys()): bt = self.connectExtractButtons[num] if bt.get_active(): self.paramd[id_].connect.mode = num @@ -234,11 +234,11 @@ def apply_(*args): self.vboxPipeline = vbox - decattrs = DecimateFilter.labels.keys() + decattrs = list(DecimateFilter.labels.keys()) decattrs.sort() self.decattrs = decattrs - names = self.paramd.keys() + names = list(self.paramd.keys()) names.sort() # Filter selection @@ -265,7 +265,7 @@ def apply_(*args): vboxFrame = gtk.VBox() vboxFrame.set_spacing(3) frameConnectFilter.add(vboxFrame) - extractModes = ConnectFilter.num2mode.items() + extractModes = list(ConnectFilter.num2mode.items()) extractModes.sort() lastButton = None self.connectExtractButtons = {} @@ -410,7 +410,7 @@ def treev_sel_changed(self, selection): 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: + except Exception as e: "During reacting to treeview selection change:", type(e), e finally: self.ignore_settings_updates = False diff --git a/pylocator/vtkNifti.py b/pylocator/vtkNifti.py index 478fae2..43549c3 100644 --- a/pylocator/vtkNifti.py +++ b/pylocator/vtkNifti.py @@ -2,8 +2,8 @@ #from numpy import oldnumeric as Numeric import numpy as np import vtk -from shared import shared -from vtkutils import array_to_vtkmatrix4x4 +from .shared import shared +from .vtkutils import array_to_vtkmatrix4x4 #from vtk.util.vtkImageImportFromArray import vtkImageImportFromArray @@ -22,9 +22,9 @@ def SetFileName(self, filename): self.__filename=filename def Update(self): - if shared.debug: print "Loading ", self.__filename + if shared.debug: print("Loading ", self.__filename) self.__nim=load(self.__filename) - if shared.debug: print self.__nim + 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) @@ -32,7 +32,7 @@ def Update(self): voxdim = self.__nim.get_header()['pixdim'][:3].copy() #Export data as string self.__data_string = self.__data.tostring() - if shared.debug: print voxdim + 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() @@ -51,9 +51,9 @@ def Update(self): affine = array_to_vtkmatrix4x4(self.__nim.get_affine()) - if shared.debug: print self._irs.GetResliceAxesOrigin() + if shared.debug: print(self._irs.GetResliceAxesOrigin()) self._irs.SetResliceAxes(affine) - if shared.debug: print self._irs.GetResliceAxesOrigin() + if shared.debug: print(self._irs.GetResliceAxesOrigin()) m2t = vtk.vtkMatrixToLinearTransform() m2t.SetInput(affine.Invert()) self._irs.TransformInputSamplingOff() @@ -62,9 +62,9 @@ def Update(self): #print self.__vtkimport.GetOutput().GetBounds() #print self._irs.GetOutput().GetBounds() - if shared.debug: print voxdim, self._irs.GetOutputSpacing() + if shared.debug: print(voxdim, self._irs.GetOutputSpacing()) self._irs.SetOutputSpacing(abs(voxdim)) - if shared.debug: print self._irs.GetOutputSpacing() + if shared.debug: print(self._irs.GetOutputSpacing()) #print self._irs.GetOutputOrigin() #self._irs.SetOutputOrigin((0,0,0)) # print self._irs.GetOutputOrigin() @@ -90,7 +90,7 @@ def GetDepth(self): return self._irs.GetOutput().GetBouds()[4:] def GetDataSpacing(self): - if shared.debug: print self.__spacing, "*******************" + if shared.debug: print(self.__spacing, "*******************") return self._irs.GetOutput().GetSpacing() def GetOutput(self): @@ -100,7 +100,7 @@ def GetOutput(self): return self._irs.GetOutput() def GetFilename(self): - if shared.debug: print self.__filename + if shared.debug: print(self.__filename) return self.__filename def GetDataExtent(self): @@ -140,5 +140,5 @@ def median(self): reader = vtkNiftiImageReader() reader.SetFileName("/home/thorsten/Dokumente/pylocator-examples/Can7/mri/post2std_brain.nii.gz") reader.Update() - print reader._irs - print reader.GetOutput() + print(reader._irs) + print(reader.GetOutput()) diff --git a/pylocator/vtksurface.py b/pylocator/vtksurface.py index 0019100..85c7c18 100644 --- a/pylocator/vtksurface.py +++ b/pylocator/vtksurface.py @@ -1,6 +1,6 @@ import vtk -from events import EventHandler -from vtkutils import vtkmatrix4x4_to_array, array_to_vtkmatrix4x4 +from .events import EventHandler +from .vtkutils import vtkmatrix4x4_to_array, array_to_vtkmatrix4x4 class VTKSurface(vtk.vtkActor): """ @@ -9,7 +9,7 @@ class VTKSurface(vtk.vtkActor): """ def set_matrix(self, registration_mat): - print "VTKSurface.set_matrix(", registration_mat, ")!!" + print("VTKSurface.set_matrix(", registration_mat, ")!!") #print "calling SetUserMatrix(", array_to_vtkmatrix4x4(registration_mat) , ")" mat = array_to_vtkmatrix4x4(registration_mat) @@ -18,7 +18,7 @@ def set_matrix(self, registration_mat): mat2xform = vtk.vtkMatrixToLinearTransform() mat2xform.SetInput(mat) - print "calling SetUserTransform(", mat2xform, ")" + print("calling SetUserTransform(", mat2xform, ")") self.SetUserTransform(mat2xform) # see vtk Prop3d docs self.Modified() # how do we like update the render tree or somethin.. @@ -126,7 +126,7 @@ def __init__(self, filename, renderer): renderer.AddActor(self.contours) # XXX: mcc will this work?!? - print "PlaneWidgetsXYZ.set_image_data: setting EventHandler.set_vtkactor(self.contours)!" + print("PlaneWidgetsXYZ.set_image_data: setting EventHandler.set_vtkactor(self.contours)!") EventHandler().set_vtkactor(self.contours) #writer = vtk.vtkSTLWriter() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e5209be --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "pylocator" +version = "1.0.0" +description = "EEG electrode localisation in MRI/CT volumes" +authors = ["Thorsten Kranz"] +license = "BSD-2-Clause" + +[tool.poetry.dependencies] +python = "^3.11" +numpy = "^1.26" +nibabel = "^5.2" +vtk = "^9.3" +PySide6 = "^6.6" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.1" + +[build-system] +requires = ["poetry-core>=1.7.0"] +build-backend = "poetry.core.masonry.api" 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)) From dcb2383b2e5cc679ae5dbe7d505333f567790208 Mon Sep 17 00:00:00 2001 From: thorstenkranz Date: Sat, 8 Nov 2025 15:59:28 +0100 Subject: [PATCH 2/7] Switch packaging to pip tooling --- README | 14 +++++- pylocator/__init__.py | 4 +- pyproject.toml | 20 --------- requirements.txt | 4 ++ setup.py | 100 ++++++++++++++---------------------------- 5 files changed, 52 insertions(+), 90 deletions(-) delete mode 100644 pyproject.toml create mode 100644 requirements.txt diff --git a/README b/README index ec246a8..e35711d 100644 --- a/README +++ b/README @@ -4,10 +4,22 @@ The PyLocator-program A little program for localization of EEG-electrodes from MRI-recordings. Uses VTK for a neat 3d-interface. +Installing dependencies +----------------------- + +The project now uses standard ``pip`` tooling. Create and activate a virtual +environment and install the runtime dependencies with:: + + pip install -r requirements.txt + +You can then install PyLocator itself in editable mode:: + + pip install -e . + Building the docs ---------------------- -To build the docs you need to have setuptools and sphinx (>=0.5) installed. +To build the docs you need to have setuptools and sphinx (>=0.5) installed. Run the command:: python setup.py build_sphinx diff --git a/pylocator/__init__.py b/pylocator/__init__.py index 29409c0..f30f10a 100644 --- a/pylocator/__init__.py +++ b/pylocator/__init__.py @@ -6,10 +6,10 @@ Dependencies: python -nibabel numpy +nibabel vtk -pygtk, gtkglext +PySide6 (GUI; currently under migration from PyGTK) """ __version__ = "1.0" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index e5209be..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[tool.poetry] -name = "pylocator" -version = "1.0.0" -description = "EEG electrode localisation in MRI/CT volumes" -authors = ["Thorsten Kranz"] -license = "BSD-2-Clause" - -[tool.poetry.dependencies] -python = "^3.11" -numpy = "^1.26" -nibabel = "^5.2" -vtk = "^9.3" -PySide6 = "^6.6" - -[tool.poetry.group.dev.dependencies] -pytest = "^8.1" - -[build-system] -requires = ["poetry-core>=1.7.0"] -build-backend = "poetry.core.masonry.api" 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.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"], +) From dd9f4df880eb54601d1132a8da89081cd61cb64e Mon Sep 17 00:00:00 2001 From: thorstenkranz Date: Sat, 8 Nov 2025 16:14:49 +0100 Subject: [PATCH 3/7] Replace GTK application with Qt port --- MANIFEST.in | 10 +- README | 64 +- doc/src/install.rst | 112 +- .../GtkGLExtVTKRenderWindowInteractor.py | 298 ----- pylocator/__init__.py | 14 +- pylocator/app.py | 45 + pylocator/colors.py | 197 ---- pylocator/connect_filter.py | 52 - pylocator/controller.py | 329 +----- pylocator/decimate_filter.py | 90 -- pylocator/dialogs.py | 159 --- pylocator/events.py | 199 ---- pylocator/gtkutils.py | 1009 ----------------- pylocator/list_toolbar.py | 39 - pylocator/main.py | 69 +- pylocator/marker_list.py | 296 ----- pylocator/marker_window_interactor.py | 280 ----- pylocator/markers.py | 222 ---- pylocator/misc/markers_io.py | 31 - pylocator/nifti_loader.py | 76 ++ pylocator/plane_widgets_observer.py | 547 --------- pylocator/plane_widgets_observer_toolbar.py | 146 --- pylocator/plane_widgets_xyz.py | 333 ------ pylocator/qt/__init__.py | 5 + pylocator/qt/main_window.py | 146 +++ pylocator/render_window.py | 282 ----- pylocator/resources.py | 21 - pylocator/resources/about.glade | 175 --- pylocator/resources/camera24.png | Bin 1383 -> 0 bytes pylocator/resources/camera48.png | Bin 3595 -> 0 bytes pylocator/resources/editCoordinates.glade | 213 ---- pylocator/resources/editLabel.glade | 128 --- pylocator/resources/editSettings.glade | 373 ------ pylocator/resources/mainWindow.glade | 605 ---------- pylocator/resources/markers.png | Bin 15397 -> 0 bytes pylocator/resources/pylocator.ico | Bin 90022 -> 0 bytes pylocator/resources/rois.png | Bin 16132 -> 0 bytes pylocator/resources/screenshots.png | Bin 12796 -> 0 bytes pylocator/resources/surfaces.png | Bin 8360 -> 0 bytes pylocator/roi_renderer_props.py | 214 ---- pylocator/rois.py | 129 --- pylocator/screenshot_props.py | 176 --- pylocator/shared.py | 37 - pylocator/surf_params.py | 134 --- pylocator/surf_renderer.py | 129 --- pylocator/surf_renderer_props.py | 479 -------- pylocator/tests/__init__.py | 0 pylocator/tests/test_gui.py | 26 - pylocator/vtkNifti.py | 144 --- pylocator/vtksurface.py | 140 --- pylocator/vtkutils.py | 32 - setup.cfg | 26 +- tests/test_nifti_loader.py | 39 + 53 files changed, 483 insertions(+), 7787 deletions(-) delete mode 100644 pylocator/GtkGLExtVTKRenderWindowInteractor.py create mode 100644 pylocator/app.py delete mode 100644 pylocator/colors.py delete mode 100644 pylocator/connect_filter.py delete mode 100644 pylocator/decimate_filter.py delete mode 100644 pylocator/dialogs.py delete mode 100644 pylocator/events.py delete mode 100644 pylocator/gtkutils.py delete mode 100644 pylocator/list_toolbar.py delete mode 100644 pylocator/marker_list.py delete mode 100644 pylocator/marker_window_interactor.py delete mode 100644 pylocator/markers.py delete mode 100644 pylocator/misc/markers_io.py create mode 100644 pylocator/nifti_loader.py delete mode 100644 pylocator/plane_widgets_observer.py delete mode 100644 pylocator/plane_widgets_observer_toolbar.py delete mode 100644 pylocator/plane_widgets_xyz.py create mode 100644 pylocator/qt/__init__.py create mode 100644 pylocator/qt/main_window.py delete mode 100644 pylocator/render_window.py delete mode 100644 pylocator/resources.py delete mode 100644 pylocator/resources/about.glade delete mode 100644 pylocator/resources/camera24.png delete mode 100644 pylocator/resources/camera48.png delete mode 100644 pylocator/resources/editCoordinates.glade delete mode 100644 pylocator/resources/editLabel.glade delete mode 100644 pylocator/resources/editSettings.glade delete mode 100644 pylocator/resources/mainWindow.glade delete mode 100644 pylocator/resources/markers.png delete mode 100644 pylocator/resources/pylocator.ico delete mode 100644 pylocator/resources/rois.png delete mode 100644 pylocator/resources/screenshots.png delete mode 100644 pylocator/resources/surfaces.png delete mode 100644 pylocator/roi_renderer_props.py delete mode 100644 pylocator/rois.py delete mode 100644 pylocator/screenshot_props.py delete mode 100644 pylocator/shared.py delete mode 100644 pylocator/surf_params.py delete mode 100644 pylocator/surf_renderer.py delete mode 100644 pylocator/surf_renderer_props.py delete mode 100644 pylocator/tests/__init__.py delete mode 100644 pylocator/tests/test_gui.py delete mode 100644 pylocator/vtkNifti.py delete mode 100644 pylocator/vtksurface.py delete mode 100644 pylocator/vtkutils.py create mode 100644 tests/test_nifti_loader.py 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 e35711d..975806f 100644 --- a/README +++ b/README @@ -1,50 +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. -Installing dependencies ------------------------ +Installation +------------ -The project now uses standard ``pip`` tooling. Create and activate a virtual -environment and install the runtime dependencies with:: +Create and activate a Python 3.11 virtual environment and install the runtime +dependencies with:: pip install -r requirements.txt -You can then install PyLocator itself in editable mode:: +You can then install PyLocator itself in editable mode while keeping the CLI +entry point available:: pip install -e . -Building the docs ----------------------- +Running PyLocator +----------------- -To build the docs you need to have setuptools and sphinx (>=0.5) installed. -Run the command:: - - python setup.py build_sphinx - -The docs are built in the build/sphinx/html directory. +Once installed you can start the GUI directly from the command line:: + pylocator -Making a source tarball ----------------------------- +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. -To create a source tarball, eg for packaging or distributing, run the -following command:: +Documentation +------------- - python setup.py sdist +The Sphinx documentation that ships with the legacy project is still available +and can be built with:: -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. - -Making a release and uploading it to PyPI ------------------------------------------- + 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/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/pylocator/GtkGLExtVTKRenderWindowInteractor.py b/pylocator/GtkGLExtVTKRenderWindowInteractor.py deleted file mode 100644 index 04f62bc..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 f30f10a..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 -numpy -nibabel -vtk -PySide6 (GUI; currently under migration from PyGTK) -""" +__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 0ee6b5e..0000000 --- a/pylocator/colors.py +++ /dev/null @@ -1,197 +0,0 @@ - - -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= list(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 16b2e59..0000000 --- a/pylocator/decimate_filter.py +++ /dev/null @@ -1,90 +0,0 @@ - -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 8b405f2..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 1d53597..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 marker in self.selected: - del self.selected[marker] - self.notify('unselect marker', marker) - - def clear_selection(self): - for oldMarker in list(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 list(self.observers.keys()): - if shared.debug: - print("EventHandler.notify(", event, "): calling update_viewer for ", observer) - try: - observer.update_viewer(event, *args) - except Exception as 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 marker in self.selected - - def get_selected(self): - return list(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 1b36494..0000000 --- a/pylocator/gtkutils.py +++ /dev/null @@ -1,1009 +0,0 @@ -import os, sys -import io, 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 = io.StringIO() - if s is not None: print(s, file=sh) - 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= list(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= list(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 list(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 as 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 as 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 as 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(','.join(row), file=fh) - 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 3c43645..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 c1835d2..0000000 --- a/pylocator/marker_list.py +++ /dev/null @@ -1,296 +0,0 @@ - -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 as 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 ac343b2..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 630c9f7..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 = list(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 7b533d6..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 c32810a..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 list(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 marker.uuid in self.ringActors: - return self.ringActors[marker.uuid] - return None - - def get_ring_actors_as_list(self): - return list(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 b2db323..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 71930ea..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 marker in self.boxes: - 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 = list(self.textActors.values()) - for actor in actors: - actor.VisibilityOn() - elif event=='labels off': - actors = list(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..2235bcb --- /dev/null +++ b/pylocator/qt/__init__.py @@ -0,0 +1,5 @@ +"""Qt user interface components for PyLocator.""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/pylocator/qt/main_window.py b/pylocator/qt/main_window.py new file mode 100644 index 0000000..c80f30b --- /dev/null +++ b/pylocator/qt/main_window.py @@ -0,0 +1,146 @@ +"""Main Qt window for the PyLocator application.""" + +from __future__ import annotations + +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QAction, QKeySequence +from PySide6.QtWidgets import ( + QDockWidget, + QMainWindow, + QTextEdit, + QToolBar, +) +from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor +from vtkmodules.vtkCommonColor import vtkColorTransferFunction +from vtkmodules.vtkCommonDataModel import vtkPiecewiseFunction +from vtkmodules.vtkRenderingCore import vtkRenderer, vtkVolume, vtkVolumeProperty +from vtkmodules.vtkRenderingVolume import vtkSmartVolumeMapper + +# VTK requires the OpenGL and interaction backends to be imported explicitly. +import vtkmodules.vtkInteractionStyle # noqa: F401 pylint: disable=unused-import +import vtkmodules.vtkRenderingOpenGL2 # noqa: F401 pylint: disable=unused-import + +from ..nifti_loader import NiftiVolume + + +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(1200, 800) + + self._current_volume: NiftiVolume | None = None + + self._create_actions() + self._create_toolbar() + self._create_vtk_view() + self._create_info_dock() + 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_vtk_view(self) -> None: + self._vtk_widget = QVTKRenderWindowInteractor(self) + self.setCentralWidget(self._vtk_widget) + self._vtk_widget.Initialize() + + render_window = self._vtk_widget.GetRenderWindow() + self._renderer = vtkRenderer() + render_window.AddRenderer(self._renderer) + self._interactor = render_window.GetInteractor() + self._interactor.Initialize() + + 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) + + # ------------------------------------------------------------------ + # Rendering helpers + # ------------------------------------------------------------------ + def display_volume(self, volume: NiftiVolume) -> None: + """Render *volume* inside the VTK viewport.""" + + self._current_volume = volume + 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._vtk_widget.GetRenderWindow().Render() + + 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/render_window.py b/pylocator/render_window.py deleted file mode 100644 index 123e983..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 uuid not in self.roi_actors: - 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 f852a4a960d9a74a501f17678791aa81f6930440..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1383 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 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 d75455069c582e09ef5a19fa9d1e45f0e37cef6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15397 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 BZdCvP diff --git a/pylocator/resources/pylocator.ico b/pylocator/resources/pylocator.ico deleted file mode 100644 index 2cd7b26b0c9fbc1abd8f8b67f7f586d7d7b6a21c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90022 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 PStV(Pq_p>xmJ;$0gTBZ5 diff --git a/pylocator/resources/rois.png b/pylocator/resources/rois.png deleted file mode 100644 index b831f18cd484ae5ef52be5600abbe3fa88f9213c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16132 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 diff --git a/pylocator/resources/screenshots.png b/pylocator/resources/screenshots.png deleted file mode 100644 index 78951afa27f20d5a984255fb0751d3621f3a6dde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12796 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 diff --git a/pylocator/resources/surfaces.png b/pylocator/resources/surfaces.png deleted file mode 100644 index 5b4d5e80b683e156c459c9416354f0af4a4c6fa0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8360 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 -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 4b45d98..0000000 --- a/pylocator/surf_params.py +++ /dev/null @@ -1,134 +0,0 @@ - -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 2c33f05..0000000 --- a/pylocator/surf_renderer.py +++ /dev/null @@ -1,129 +0,0 @@ - -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 = list(self.textActors.values()) - for actor in actors: - actor.VisibilityOn() - else: - actors = list(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 uuid not in self.surface_actors: - 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 = list(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 8a29660..0000000 --- a/pylocator/surf_renderer_props.py +++ /dev/null @@ -1,479 +0,0 @@ - - -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 list(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 = list(DecimateFilter.labels.keys()) - decattrs.sort() - self.decattrs = decattrs - - names = list(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 = list(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 as 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 43549c3..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 85c7c18..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/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/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)) From 8c686d782fa9a2d06bb40c4e376af18aaa4748bb Mon Sep 17 00:00:00 2001 From: thorstenkranz Date: Sat, 8 Nov 2025 16:18:00 +0100 Subject: [PATCH 4/7] Fix VTK color transfer import for Qt window --- pylocator/qt/main_window.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pylocator/qt/main_window.py b/pylocator/qt/main_window.py index c80f30b..7fd122f 100644 --- a/pylocator/qt/main_window.py +++ b/pylocator/qt/main_window.py @@ -11,9 +11,13 @@ QToolBar, ) from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor -from vtkmodules.vtkCommonColor import vtkColorTransferFunction from vtkmodules.vtkCommonDataModel import vtkPiecewiseFunction -from vtkmodules.vtkRenderingCore import vtkRenderer, vtkVolume, vtkVolumeProperty +from vtkmodules.vtkRenderingCore import ( + vtkColorTransferFunction, + vtkRenderer, + vtkVolume, + vtkVolumeProperty, +) from vtkmodules.vtkRenderingVolume import vtkSmartVolumeMapper # VTK requires the OpenGL and interaction backends to be imported explicitly. From 42cdc4b69a358ff962f94a14ac9e78bbe805f2a5 Mon Sep 17 00:00:00 2001 From: thorstenkranz Date: Sat, 8 Nov 2025 16:25:07 +0100 Subject: [PATCH 5/7] Fix VTK smart volume mapper import --- pylocator/qt/main_window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pylocator/qt/main_window.py b/pylocator/qt/main_window.py index 7fd122f..4557c30 100644 --- a/pylocator/qt/main_window.py +++ b/pylocator/qt/main_window.py @@ -18,7 +18,10 @@ vtkVolume, vtkVolumeProperty, ) -from vtkmodules.vtkRenderingVolume import vtkSmartVolumeMapper +try: + from vtkmodules.vtkRenderingVolumeOpenGL2 import vtkSmartVolumeMapper +except ImportError: # pragma: no cover - fallback for alternative VTK builds + from vtkmodules.vtkRenderingVolume import vtkSmartVolumeMapper # VTK requires the OpenGL and interaction backends to be imported explicitly. import vtkmodules.vtkInteractionStyle # noqa: F401 pylint: disable=unused-import From 938c2367ca3f15b90c9a7cc6728515c75f18c827 Mon Sep 17 00:00:00 2001 From: thorstenkranz Date: Sat, 8 Nov 2025 16:42:00 +0100 Subject: [PATCH 6/7] Add multi-viewport Qt layout with slice controls --- pylocator/qt/__init__.py | 5 +- pylocator/qt/main_window.py | 125 +++++++++++++------------ pylocator/qt/views.py | 181 ++++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 58 deletions(-) create mode 100644 pylocator/qt/views.py diff --git a/pylocator/qt/__init__.py b/pylocator/qt/__init__.py index 2235bcb..7b42adb 100644 --- a/pylocator/qt/__init__.py +++ b/pylocator/qt/__init__.py @@ -2,4 +2,7 @@ from __future__ import annotations -__all__: list[str] = [] +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 index 4557c30..1035a6a 100644 --- a/pylocator/qt/main_window.py +++ b/pylocator/qt/main_window.py @@ -2,32 +2,24 @@ 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 vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor -from vtkmodules.vtkCommonDataModel import vtkPiecewiseFunction -from vtkmodules.vtkRenderingCore import ( - vtkColorTransferFunction, - vtkRenderer, - vtkVolume, - vtkVolumeProperty, -) -try: - from vtkmodules.vtkRenderingVolumeOpenGL2 import vtkSmartVolumeMapper -except ImportError: # pragma: no cover - fallback for alternative VTK builds - from vtkmodules.vtkRenderingVolume import vtkSmartVolumeMapper - -# VTK requires the OpenGL and interaction backends to be imported explicitly. -import vtkmodules.vtkInteractionStyle # noqa: F401 pylint: disable=unused-import -import vtkmodules.vtkRenderingOpenGL2 # noqa: F401 pylint: disable=unused-import from ..nifti_loader import NiftiVolume +from .views import SliceView, VolumeView class MainWindow(QMainWindow): @@ -38,14 +30,15 @@ class MainWindow(QMainWindow): def __init__(self, parent=None) -> None: super().__init__(parent) self.setWindowTitle("PyLocator") - self.resize(1200, 800) + self.resize(1400, 900) self._current_volume: NiftiVolume | None = None self._create_actions() self._create_toolbar() - self._create_vtk_view() + self._create_views() self._create_info_dock() + self._create_slice_controls() self.statusBar().showMessage("Ready") # ------------------------------------------------------------------ @@ -71,16 +64,27 @@ def _create_toolbar(self) -> None: toolbar.addAction(self.open_action) self.addToolBar(toolbar) - def _create_vtk_view(self) -> None: - self._vtk_widget = QVTKRenderWindowInteractor(self) - self.setCentralWidget(self._vtk_widget) - self._vtk_widget.Initialize() + 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) - render_window = self._vtk_widget.GetRenderWindow() - self._renderer = vtkRenderer() - render_window.AddRenderer(self._renderer) - self._interactor = render_window.GetInteractor() - self._interactor.Initialize() + 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) @@ -92,42 +96,49 @@ def _create_info_dock(self) -> None: 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 viewport.""" + """Render *volume* inside the VTK viewports.""" self._current_volume = volume - 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._vtk_widget.GetRenderWindow().Render() + 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}") diff --git a/pylocator/qt/views.py b/pylocator/qt/views.py new file mode 100644 index 0000000..0424f79 --- /dev/null +++ b/pylocator/qt/views.py @@ -0,0 +1,181 @@ +"""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.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) -> 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() + + 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) + 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) + 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"] From 76f6f81632a13286af309c51e33ffcbe643eefe1 Mon Sep 17 00:00:00 2001 From: thorstenkranz Date: Sat, 8 Nov 2025 16:46:40 +0100 Subject: [PATCH 7/7] Restore interactor styles for VTK views --- pylocator/qt/views.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pylocator/qt/views.py b/pylocator/qt/views.py index 0424f79..7f4699e 100644 --- a/pylocator/qt/views.py +++ b/pylocator/qt/views.py @@ -7,6 +7,10 @@ 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, @@ -37,7 +41,12 @@ class SliceGeometry: class _BaseVTKView: """Common helpers for views backed by :class:`QVTKRenderWindowInteractor`.""" - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, + parent: QWidget | None = None, + *, + interactor_style: type | None = None, + ) -> None: self.widget = QVTKRenderWindowInteractor(parent) self.widget.Initialize() self.renderer = vtkRenderer() @@ -45,6 +54,11 @@ def __init__(self, parent: QWidget | None = None) -> None: 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.""" @@ -56,7 +70,7 @@ class VolumeView(_BaseVTKView): """3-D volume rendering viewport.""" def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + super().__init__(parent, interactor_style=vtkInteractorStyleTrackballCamera) self.renderer.SetBackground(0.1, 0.1, 0.1) def set_volume(self, volume: NiftiVolume) -> None: @@ -97,7 +111,7 @@ class SliceView(_BaseVTKView): """Orthogonal 2-D slice view into the volume.""" def __init__(self, orientation: str, parent: QWidget | None = None) -> None: - super().__init__(parent) + super().__init__(parent, interactor_style=vtkInteractorStyleImage) self.orientation = orientation self._volume: NiftiVolume | None = None self._mapper = vtkImageSliceMapper()