Commit af27358df57f107ac4f4e4b5830a582dc614d2ea

Authored by okahilak
Committed by GitHub
2 parents 893d1a70 58d46d18
Exists in master

Merge branch 'master' into export-world-coordinates-into-marker-file

1 -invesalius/*.classpath  
2 -invesalius/*.hg  
3 -invesalius/*.log  
4 -invesalius/*.project  
5 -invesalius/*.pyc  
6 -invesalius/*.swn  
7 -invesalius/*.swo  
8 -invesalius/*.swp  
9 -invesalius/data/*.log  
10 -invesalius/data/*.pyc  
11 -invesalius/data/*.pyd  
12 -invesalius/gui/*.log  
13 -invesalius/gui/*.pyc  
14 -invesalius/gui/widgets/*.log  
15 -invesalius/gui/widgets/*.pyc  
16 -invesalius/reader/*.log  
17 -invesalius/reader/*.pyc  
18 -  
19 -.idea  
20 -  
21 -*.pyc  
22 -*.swp 1 +# Created by https://www.toptal.com/developers/gitignore/api/vim,python,intellij,visualstudiocode,direnv
  2 +# Edit at https://www.toptal.com/developers/gitignore?templates=vim,python,intellij,visualstudiocode,direnv
  3 +
  4 +### direnv ###
  5 +.direnv
  6 +.envrc
  7 +
  8 +### Intellij ###
  9 +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
  10 +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
  11 +
  12 +# User-specific stuff
  13 +.idea/**/workspace.xml
  14 +.idea/**/tasks.xml
  15 +.idea/**/usage.statistics.xml
  16 +.idea/**/dictionaries
  17 +.idea/**/shelf
  18 +
  19 +# AWS User-specific
  20 +.idea/**/aws.xml
  21 +
  22 +# Generated files
  23 +.idea/**/contentModel.xml
  24 +
  25 +# Sensitive or high-churn files
  26 +.idea/**/dataSources/
  27 +.idea/**/dataSources.ids
  28 +.idea/**/dataSources.local.xml
  29 +.idea/**/sqlDataSources.xml
  30 +.idea/**/dynamic.xml
  31 +.idea/**/uiDesigner.xml
  32 +.idea/**/dbnavigator.xml
  33 +
  34 +# Gradle
  35 +.idea/**/gradle.xml
  36 +.idea/**/libraries
  37 +
  38 +# Gradle and Maven with auto-import
  39 +# When using Gradle or Maven with auto-import, you should exclude module files,
  40 +# since they will be recreated, and may cause churn. Uncomment if using
  41 +# auto-import.
  42 +# .idea/artifacts
  43 +# .idea/compiler.xml
  44 +# .idea/jarRepositories.xml
  45 +# .idea/modules.xml
  46 +# .idea/*.iml
  47 +# .idea/modules
  48 +# *.iml
  49 +# *.ipr
  50 +
  51 +# CMake
  52 +cmake-build-*/
  53 +
  54 +# Mongo Explorer plugin
  55 +.idea/**/mongoSettings.xml
  56 +
  57 +# File-based project format
  58 +*.iws
  59 +
  60 +# IntelliJ
  61 +out/
  62 +
  63 +# mpeltonen/sbt-idea plugin
  64 +.idea_modules/
  65 +
  66 +# JIRA plugin
  67 +atlassian-ide-plugin.xml
  68 +
  69 +# Cursive Clojure plugin
  70 +.idea/replstate.xml
  71 +
  72 +# Crashlytics plugin (for Android Studio and IntelliJ)
  73 +com_crashlytics_export_strings.xml
  74 +crashlytics.properties
  75 +crashlytics-build.properties
  76 +fabric.properties
  77 +
  78 +# Editor-based Rest Client
  79 +.idea/httpRequests
  80 +
  81 +# Android studio 3.1+ serialized cache file
  82 +.idea/caches/build_file_checksums.ser
  83 +
  84 +### Intellij Patch ###
  85 +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
  86 +
  87 +# *.iml
  88 +# modules.xml
  89 +# .idea/misc.xml
  90 +# *.ipr
  91 +
  92 +# Sonarlint plugin
  93 +# https://plugins.jetbrains.com/plugin/7973-sonarlint
  94 +.idea/**/sonarlint/
  95 +
  96 +# SonarQube Plugin
  97 +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
  98 +.idea/**/sonarIssues.xml
  99 +
  100 +# Markdown Navigator plugin
  101 +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
  102 +.idea/**/markdown-navigator.xml
  103 +.idea/**/markdown-navigator-enh.xml
  104 +.idea/**/markdown-navigator/
  105 +
  106 +# Cache file creation bug
  107 +# See https://youtrack.jetbrains.com/issue/JBR-2257
  108 +.idea/$CACHE_FILE$
  109 +
  110 +# CodeStream plugin
  111 +# https://plugins.jetbrains.com/plugin/12206-codestream
  112 +.idea/codestream.xml
  113 +
  114 +### Python ###
  115 +# Byte-compiled / optimized / DLL files
  116 +__pycache__/
  117 +*.py[cod]
  118 +*$py.class
  119 +
  120 +# C extensions
23 *.so 121 *.so
24 -tags  
25 -*.c  
26 122
27 -.idea  
28 -build  
29 -*.patch  
30 -*.tgz 123 +# Distribution / packaging
  124 +.Python
  125 +build/
  126 +develop-eggs/
  127 +dist/
  128 +downloads/
  129 +eggs/
  130 +.eggs/
  131 +lib/
  132 +lib64/
  133 +parts/
  134 +sdist/
  135 +var/
  136 +wheels/
  137 +share/python-wheels/
  138 +*.egg-info/
  139 +.installed.cfg
  140 +*.egg
  141 +MANIFEST
31 142
32 -*.pyd  
33 -*.cpp  
34 -*.diff 143 +# PyInstaller
  144 +# Usually these files are written by a python script from a template
  145 +# before PyInstaller builds the exe, so as to inject date/other infos into it.
  146 +*.manifest
  147 +*.spec
  148 +
  149 +# Installer logs
  150 +pip-log.txt
  151 +pip-delete-this-directory.txt
  152 +
  153 +# Unit test / coverage reports
  154 +htmlcov/
  155 +.tox/
  156 +.nox/
  157 +.coverage
  158 +.coverage.*
  159 +.cache
  160 +nosetests.xml
  161 +coverage.xml
  162 +*.cover
  163 +*.py,cover
  164 +.hypothesis/
  165 +.pytest_cache/
  166 +cover/
  167 +
  168 +# Translations
  169 +*.mo
  170 +*.pot
  171 +
  172 +# Django stuff:
  173 +*.log
  174 +local_settings.py
  175 +db.sqlite3
  176 +db.sqlite3-journal
  177 +
  178 +# Flask stuff:
  179 +instance/
  180 +.webassets-cache
  181 +
  182 +# Scrapy stuff:
  183 +.scrapy
35 184
36 -*.directory 185 +# Sphinx documentation
  186 +docs/_build/
37 187
  188 +# PyBuilder
  189 +.pybuilder/
  190 +target/
38 191
39 -# latex 192 +# Jupyter Notebook
  193 +.ipynb_checkpoints
  194 +
  195 +# IPython
  196 +profile_default/
  197 +ipython_config.py
  198 +
  199 +# pyenv
  200 +# For a library or package, you might want to ignore these files since the code is
  201 +# intended to run in multiple environments; otherwise, check them in:
  202 +# .python-version
  203 +
  204 +# pipenv
  205 +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
  206 +# However, in case of collaboration, if having platform-specific dependencies or dependencies
  207 +# having no cross-platform support, pipenv may install dependencies that don't work, or not
  208 +# install all needed dependencies.
  209 +#Pipfile.lock
  210 +
  211 +# PEP 582; used by e.g. github.com/David-OConnor/pyflow
  212 +__pypackages__/
  213 +
  214 +# Celery stuff
  215 +celerybeat-schedule
  216 +celerybeat.pid
  217 +
  218 +# SageMath parsed files
  219 +*.sage.py
  220 +
  221 +# Environments
  222 +.env
  223 +.venv
  224 +env/
  225 +venv/
  226 +ENV/
  227 +env.bak/
  228 +venv.bak/
  229 +myenv/
  230 +
  231 +# Spyder project settings
  232 +.spyderproject
  233 +.spyproject
  234 +
  235 +# Rope project settings
  236 +.ropeproject
  237 +
  238 +# mkdocs documentation
  239 +/site
  240 +
  241 +# mypy
  242 +.mypy_cache/
  243 +.dmypy.json
  244 +dmypy.json
  245 +
  246 +# Pyre type checker
  247 +.pyre/
  248 +
  249 +# pytype static type analyzer
  250 +.pytype/
  251 +
  252 +# Cython debug symbols
  253 +cython_debug/
  254 +
  255 +### Vim ###
  256 +# Swap
  257 +[._]*.s[a-v][a-z]
  258 +!*.svg # comment out if you don't need vector files
  259 +[._]*.sw[a-p]
  260 +[._]s[a-rt-v][a-z]
  261 +[._]ss[a-gi-z]
  262 +[._]sw[a-p]
  263 +
  264 +# Session
  265 +Session.vim
  266 +Sessionx.vim
  267 +
  268 +# Temporary
  269 +.netrwhist
  270 +*~
  271 +# Auto-generated tag files
  272 +tags
  273 +# Persistent undo
  274 +[._]*.un~
  275 +
  276 +### VisualStudioCode ###
  277 +.vscode/*
  278 +*.code-workspace
  279 +
  280 +# Local History for Visual Studio Code
  281 +.history/
  282 +
  283 +### VisualStudioCode Patch ###
  284 +# Ignore all local history of files
  285 +.history
  286 +.ionide
  287 +
  288 +### LaTeX ###
  289 +## Core latex/pdflatex auxiliary files:
40 *.aux 290 *.aux
  291 +*.lof
  292 +*.log
  293 +*.lot
  294 +*.fls
  295 +*.out
41 *.toc 296 *.toc
  297 +*.fmt
  298 +*.fot
  299 +*.cb
  300 +*.cb2
  301 +.*.lb
  302 +
  303 +## Intermediate documents:
  304 +*.dvi
  305 +*.xdv
  306 +*-converted-to.*
  307 +# these rules might exclude image files for figures etc.
  308 +# *.ps
  309 +# *.eps
  310 +# *.pdf
  311 +
  312 +## Generated if empty string is given at "Please type another file name for output:"
  313 +.pdf
  314 +
  315 +## Bibliography auxiliary files (bibtex/biblatex/biber):
42 *.bbl 316 *.bbl
  317 +*.bcf
43 *.blg 318 *.blg
44 -*.fls 319 +*-blx.aux
  320 +*-blx.bib
  321 +*.run.xml
  322 +
  323 +## Build tool auxiliary files:
45 *.fdb_latexmk 324 *.fdb_latexmk
  325 +*.synctex
  326 +*.synctex(busy)
46 *.synctex.gz 327 *.synctex.gz
47 -*.out  
48 -*.log 328 +*.synctex.gz(busy)
  329 +*.pdfsync
  330 +
  331 +# End of https://www.toptal.com/developers/gitignore/api/vim,python,intellij,visualstudiocode,direnv
  332 +
  333 +### Cython generated files ###
  334 +*.c
  335 +*.cpp
  336 +
  337 +### Patches and diffs ###
  338 +*.patch
  339 +*.diff
invesalius/constants.py
@@ -761,17 +761,38 @@ OBJA = wx.NewId() @@ -761,17 +761,38 @@ OBJA = wx.NewId()
761 OBJC = wx.NewId() 761 OBJC = wx.NewId()
762 OBJF = wx.NewId() 762 OBJF = wx.NewId()
763 763
764 -BTNS_OBJ = {OBJL: {0: _('Left')},  
765 - OBJR: {1: _('Right')},  
766 - OBJA: {2: _('Anterior')},  
767 - OBJC: {3: _('Center')},  
768 - OBJF: {4: _('Fixed')}}  
769 -  
770 -TIPS_OBJ = [_("Select left object fiducial"),  
771 - _("Select right object fiducial"),  
772 - _("Select anterior object fiducial"),  
773 - _("Select object center"),  
774 - _("Attach sensor to object")] 764 +OBJECT_FIDUCIALS = [
  765 + {
  766 + 'fiducial_index': 0,
  767 + 'button_id': OBJL,
  768 + 'label': _('Left'),
  769 + 'tip': _("Select left object fiducial"),
  770 + },
  771 + {
  772 + 'fiducial_index': 1,
  773 + 'button_id': OBJR,
  774 + 'label': _('Right'),
  775 + 'tip': _("Select right object fiducial"),
  776 + },
  777 + {
  778 + 'fiducial_index': 2,
  779 + 'button_id': OBJA,
  780 + 'label': _('Anterior'),
  781 + 'tip': _("Select anterior object fiducial"),
  782 + },
  783 + {
  784 + 'fiducial_index': 3,
  785 + 'button_id': OBJC,
  786 + 'label': _('Center'),
  787 + 'tip': _("Select object center"),
  788 + },
  789 + {
  790 + 'fiducial_index': 4,
  791 + 'button_id': OBJF,
  792 + 'label': _('Fixed'),
  793 + 'tip': _("Attach sensor to object"),
  794 + },
  795 +]
775 796
776 MTC_PROBE_NAME = "1Probe" 797 MTC_PROBE_NAME = "1Probe"
777 MTC_REF_NAME = "2Ref" 798 MTC_REF_NAME = "2Ref"
invesalius/data/bases.py
@@ -188,6 +188,13 @@ def object_registration(fiducials, orients, coord_raw, m_change): @@ -188,6 +188,13 @@ def object_registration(fiducials, orients, coord_raw, m_change):
188 fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3] 188 fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3]
189 189
190 # compute initial alignment of probe fixed in the object in source frame 190 # compute initial alignment of probe fixed in the object in source frame
  191 +
  192 + # XXX: Some duplicate processing is done here: the Euler angles are calculated once by
  193 + # the lines below, and then again in dco.coordinates_to_transformation_matrix.
  194 + #
  195 + a, b, g = np.radians(coords[3, 3:])
  196 + r_s0_raw = tr.euler_matrix(a, b, g, axes='rzyx')
  197 +
191 s0_raw = dco.coordinates_to_transformation_matrix( 198 s0_raw = dco.coordinates_to_transformation_matrix(
192 position=coords[3, :3], 199 position=coords[3, :3],
193 orientation=coords[3, 3:], 200 orientation=coords[3, 3:],
invesalius/data/converters.py
@@ -149,6 +149,7 @@ def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)): @@ -149,6 +149,7 @@ def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)):
149 # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html 149 # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html
150 def gdcm_to_numpy(image, apply_intercep_scale=True): 150 def gdcm_to_numpy(image, apply_intercep_scale=True):
151 map_gdcm_np = { 151 map_gdcm_np = {
  152 + gdcm.PixelFormat.SINGLEBIT: np.uint8,
152 gdcm.PixelFormat.UINT8: np.uint8, 153 gdcm.PixelFormat.UINT8: np.uint8,
153 gdcm.PixelFormat.INT8: np.int8, 154 gdcm.PixelFormat.INT8: np.int8,
154 gdcm.PixelFormat.UINT12: np.uint16, 155 gdcm.PixelFormat.UINT12: np.uint16,
@@ -177,6 +178,8 @@ def gdcm_to_numpy(image, apply_intercep_scale=True): @@ -177,6 +178,8 @@ def gdcm_to_numpy(image, apply_intercep_scale=True):
177 np_array = np.frombuffer( 178 np_array = np.frombuffer(
178 gdcm_array.encode("utf-8", errors="surrogateescape"), dtype=dtype 179 gdcm_array.encode("utf-8", errors="surrogateescape"), dtype=dtype
179 ) 180 )
  181 + if pf.GetScalarType() == gdcm.PixelFormat.SINGLEBIT:
  182 + np_array = np.unpackbits(np_array)
180 np_array.shape = shape 183 np_array.shape = shape
181 np_array = np_array.squeeze() 184 np_array = np_array.squeeze()
182 185
invesalius/gui/dialogs.py
@@ -3304,8 +3304,9 @@ class MaskDensityDialog(wx.Dialog): @@ -3304,8 +3304,9 @@ class MaskDensityDialog(wx.Dialog):
3304 3304
3305 class ObjectCalibrationDialog(wx.Dialog): 3305 class ObjectCalibrationDialog(wx.Dialog):
3306 3306
3307 - def __init__(self, tracker): 3307 + def __init__(self, tracker, pedal_connection):
3308 self.tracker = tracker 3308 self.tracker = tracker
  3309 + self.pedal_connection = pedal_connection
3309 3310
3310 self.trk_init, self.tracker_id = tracker.GetTrackerInfo() 3311 self.trk_init, self.tracker_id = tracker.GetTrackerInfo()
3311 3312
@@ -3313,6 +3314,7 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3313,6 +3314,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3313 self.obj_name = None 3314 self.obj_name = None
3314 self.polydata = None 3315 self.polydata = None
3315 self.use_default_object = False 3316 self.use_default_object = False
  3317 + self.object_fiducial_being_set = None
3316 3318
3317 self.obj_fiducials = np.full([5, 3], np.nan) 3319 self.obj_fiducials = np.full([5, 3], np.nan)
3318 self.obj_orients = np.full([5, 3], np.nan) 3320 self.obj_orients = np.full([5, 3], np.nan)
@@ -3323,6 +3325,11 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3323,6 +3325,11 @@ class ObjectCalibrationDialog(wx.Dialog):
3323 self._init_gui() 3325 self._init_gui()
3324 self.LoadObject() 3326 self.LoadObject()
3325 3327
  3328 + self.__bind_events()
  3329 +
  3330 + def __bind_events(self):
  3331 + Publisher.subscribe(self.SetObjectFiducial, 'Set object fiducial')
  3332 +
3326 def _init_gui(self): 3333 def _init_gui(self):
3327 self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) 3334 self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize())
3328 self.interactor.Enable(1) 3335 self.interactor.Enable(1)
@@ -3373,15 +3380,17 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3373,15 +3380,17 @@ class ObjectCalibrationDialog(wx.Dialog):
3373 choice_sensor]) 3380 choice_sensor])
3374 3381
3375 # Push buttons for object fiducials 3382 # Push buttons for object fiducials
3376 - btns_obj = const.BTNS_OBJ  
3377 - tips_obj = const.TIPS_OBJ 3383 + for object_fiducial in const.OBJECT_FIDUCIALS:
  3384 + index = object_fiducial['fiducial_index']
  3385 + label = object_fiducial['label']
  3386 + button_id = object_fiducial['button_id']
  3387 + tip = object_fiducial['tip']
3378 3388
3379 - for k in btns_obj:  
3380 - n = list(btns_obj[k].keys())[0]  
3381 - lab = list(btns_obj[k].values())[0]  
3382 - self.btns_coord[n] = wx.Button(self, k, label=lab, size=wx.Size(60, 23))  
3383 - self.btns_coord[n].SetToolTip(wx.ToolTip(tips_obj[n]))  
3384 - self.btns_coord[n].Bind(wx.EVT_BUTTON, self.OnGetObjectFiducials) 3389 + ctrl = wx.ToggleButton(self, button_id, label=label, size=wx.Size(60, 23))
  3390 + ctrl.SetToolTip(wx.ToolTip(tip))
  3391 + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnObjectFiducialButton, index, ctrl=ctrl))
  3392 +
  3393 + self.btns_coord[index] = ctrl
3385 3394
3386 for m in range(0, 5): 3395 for m in range(0, 5):
3387 for n in range(0, 3): 3396 for n in range(0, 3):
@@ -3525,13 +3534,42 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3525,13 +3534,42 @@ class ObjectCalibrationDialog(wx.Dialog):
3525 self.ren.AddActor(ball_actor) 3534 self.ren.AddActor(ball_actor)
3526 return ball_actor, tactor 3535 return ball_actor, tactor
3527 3536
3528 - def OnGetObjectFiducials(self, evt): 3537 + def OnObjectFiducialButton(self, index, evt, ctrl):
3529 if not self.tracker.IsTrackerInitialized(): 3538 if not self.tracker.IsTrackerInitialized():
3530 ShowNavigationTrackerWarning(0, 'choose') 3539 ShowNavigationTrackerWarning(0, 'choose')
3531 return 3540 return
3532 3541
3533 - btn_id = list(const.BTNS_OBJ[evt.GetId()].keys())[0] 3542 + # TODO: The code below until the end of the function is essentially copy-paste from
  3543 + # OnTrackerFiducials function in NeuronavigationPanel class. Probably the easiest
  3544 + # way to deduplicate this would be to create a Fiducial class, which would contain
  3545 + # this code just once.
  3546 + #
  3547 +
  3548 + # Do not allow several object fiducials to be set at the same time.
  3549 + if self.object_fiducial_being_set is not None and self.object_fiducial_being_set != index:
  3550 + ctrl.SetValue(False)
  3551 + return
  3552 +
  3553 + # Called when the button for setting the object fiducial is enabled and either pedal is pressed
  3554 + # or the button is pressed again.
  3555 + #
  3556 + def set_fiducial_callback():
  3557 + Publisher.sendMessage('Set object fiducial', fiducial_index=index)
  3558 + if self.pedal_connection is not None:
  3559 + self.pedal_connection.remove_callback('fiducial')
  3560 +
  3561 + ctrl.SetValue(False)
  3562 + self.object_fiducial_being_set = None
  3563 +
  3564 + if ctrl.GetValue():
  3565 + self.object_fiducial_being_set = index
  3566 +
  3567 + if self.pedal_connection is not None:
  3568 + self.pedal_connection.add_callback('fiducial', set_fiducial_callback)
  3569 + else:
  3570 + set_fiducial_callback()
3534 3571
  3572 + def SetObjectFiducial(self, fiducial_index):
3535 coord, coord_raw = self.tracker.GetTrackerCoordinates( 3573 coord, coord_raw = self.tracker.GetTrackerCoordinates(
3536 # XXX: Always use static reference mode when getting the coordinates. This is what the 3574 # XXX: Always use static reference mode when getting the coordinates. This is what the
3537 # code did previously, as well. At some point, it should probably be thought through 3575 # code did previously, as well. At some point, it should probably be thought through
@@ -3549,22 +3587,22 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3549,22 +3587,22 @@ class ObjectCalibrationDialog(wx.Dialog):
3549 # mode" principle above, but it's hard to come up with a simple change to increase the consistency 3587 # mode" principle above, but it's hard to come up with a simple change to increase the consistency
3550 # and not change the function to the point of potentially breaking it.) 3588 # and not change the function to the point of potentially breaking it.)
3551 # 3589 #
3552 - if self.obj_ref_id and btn_id == 4: 3590 + if self.obj_ref_id and fiducial_index == 4:
3553 coord = coord_raw[self.obj_ref_id, :] 3591 coord = coord_raw[self.obj_ref_id, :]
3554 coord[2] = -coord[2] 3592 coord[2] = -coord[2]
3555 3593
3556 - if btn_id == 3: 3594 + if fiducial_index == 3:
3557 coord = np.zeros([6,]) 3595 coord = np.zeros([6,])
3558 3596
3559 # Update text controls with tracker coordinates 3597 # Update text controls with tracker coordinates
3560 if coord is not None or np.sum(coord) != 0.0: 3598 if coord is not None or np.sum(coord) != 0.0:
3561 - self.obj_fiducials[btn_id, :] = coord[:3]  
3562 - self.obj_orients[btn_id, :] = coord[3:]  
3563 - for n in [0, 1, 2]:  
3564 - self.txt_coord[btn_id][n].SetLabel(str(round(coord[n], 1)))  
3565 - if self.text_actors[btn_id]:  
3566 - self.text_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0)  
3567 - self.ball_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0) 3599 + self.obj_fiducials[fiducial_index, :] = coord[:3]
  3600 + self.obj_orients[fiducial_index, :] = coord[3:]
  3601 + for i in [0, 1, 2]:
  3602 + self.txt_coord[fiducial_index][i].SetLabel(str(round(coord[i], 1)))
  3603 + if self.text_actors[fiducial_index]:
  3604 + self.text_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0)
  3605 + self.ball_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0)
3568 self.Refresh() 3606 self.Refresh()
3569 else: 3607 else:
3570 ShowNavigationTrackerWarning(0, 'choose') 3608 ShowNavigationTrackerWarning(0, 'choose')
invesalius/gui/task_navigator.py
@@ -18,12 +18,9 @@ @@ -18,12 +18,9 @@
18 #-------------------------------------------------------------------------- 18 #--------------------------------------------------------------------------
19 19
20 from functools import partial 20 from functools import partial
  21 +import itertools
21 import csv 22 import csv
22 -import os  
23 -import queue  
24 -import sys  
25 import time 23 import time
26 -import threading  
27 24
28 import nibabel as nb 25 import nibabel as nb
29 import numpy as np 26 import numpy as np
@@ -35,10 +32,8 @@ except ImportError: @@ -35,10 +32,8 @@ except ImportError:
35 import wx 32 import wx
36 33
37 try: 34 try:
38 - import wx.lib.agw.hyperlink as hl  
39 import wx.lib.agw.foldpanelbar as fpb 35 import wx.lib.agw.foldpanelbar as fpb
40 except ImportError: 36 except ImportError:
41 - import wx.lib.hyperlink as hl  
42 import wx.lib.foldpanelbar as fpb 37 import wx.lib.foldpanelbar as fpb
43 38
44 import wx.lib.colourselect as csel 39 import wx.lib.colourselect as csel
@@ -47,25 +42,22 @@ from invesalius.pubsub import pub as Publisher @@ -47,25 +42,22 @@ from invesalius.pubsub import pub as Publisher
47 from time import sleep 42 from time import sleep
48 43
49 import invesalius.constants as const 44 import invesalius.constants as const
50 -import invesalius.data.bases as db  
51 45
52 if has_trekker: 46 if has_trekker:
53 import invesalius.data.brainmesh_handler as brain 47 import invesalius.data.brainmesh_handler as brain
54 48
55 -import invesalius.data.coordinates as dco  
56 -import invesalius.data.coregistration as dcr  
57 import invesalius.data.imagedata_utils as imagedata_utils 49 import invesalius.data.imagedata_utils as imagedata_utils
58 -import invesalius.data.serial_port_connection as spc  
59 import invesalius.data.slice_ as sl 50 import invesalius.data.slice_ as sl
60 -import invesalius.data.trackers as dt  
61 import invesalius.data.tractography as dti 51 import invesalius.data.tractography as dti
62 -import invesalius.data.transformations as tr  
63 import invesalius.data.record_coords as rec 52 import invesalius.data.record_coords as rec
64 import invesalius.data.vtk_utils as vtk_utils 53 import invesalius.data.vtk_utils as vtk_utils
65 import invesalius.gui.dialogs as dlg 54 import invesalius.gui.dialogs as dlg
66 import invesalius.project as prj 55 import invesalius.project as prj
67 from invesalius import utils 56 from invesalius import utils
68 from invesalius.gui import utils as gui_utils 57 from invesalius.gui import utils as gui_utils
  58 +from invesalius.navigation.icp import ICP
  59 +from invesalius.navigation.navigation import Navigation
  60 +from invesalius.navigation.tracker import Tracker
69 61
70 HAS_PEDAL_CONNECTION = True 62 HAS_PEDAL_CONNECTION = True
71 try: 63 try:
@@ -161,9 +153,10 @@ class InnerFoldPanel(wx.Panel): @@ -161,9 +153,10 @@ class InnerFoldPanel(wx.Panel):
161 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, 153 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
162 (10, 310), 0, fpb.FPB_SINGLE_FOLD) 154 (10, 310), 0, fpb.FPB_SINGLE_FOLD)
163 155
164 - # Initialize Tracker object here so that it is available to several panels. 156 + # Initialize Tracker and PedalConnection objects here so that they are available to several panels.
165 # 157 #
166 tracker = Tracker() 158 tracker = Tracker()
  159 + pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None
167 160
168 # Fold panel style 161 # Fold panel style
169 style = fpb.CaptionBarStyle() 162 style = fpb.CaptionBarStyle()
@@ -173,7 +166,7 @@ class InnerFoldPanel(wx.Panel): @@ -173,7 +166,7 @@ class InnerFoldPanel(wx.Panel):
173 166
174 # Fold 1 - Navigation panel 167 # Fold 1 - Navigation panel
175 item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) 168 item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True)
176 - ntw = NeuronavigationPanel(item, tracker) 169 + ntw = NeuronavigationPanel(item, tracker, pedal_connection)
177 170
178 fold_panel.ApplyCaptionStyle(item, style) 171 fold_panel.ApplyCaptionStyle(item, style)
179 fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, 172 fold_panel.AddFoldPanelWindow(item, ntw, spacing=0,
@@ -182,7 +175,7 @@ class InnerFoldPanel(wx.Panel): @@ -182,7 +175,7 @@ class InnerFoldPanel(wx.Panel):
182 175
183 # Fold 2 - Object registration panel 176 # Fold 2 - Object registration panel
184 item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) 177 item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True)
185 - otw = ObjectRegistrationPanel(item, tracker) 178 + otw = ObjectRegistrationPanel(item, tracker, pedal_connection)
186 179
187 fold_panel.ApplyCaptionStyle(item, style) 180 fold_panel.ApplyCaptionStyle(item, style)
188 fold_panel.AddFoldPanelWindow(item, otw, spacing=0, 181 fold_panel.AddFoldPanelWindow(item, otw, spacing=0,
@@ -315,390 +308,8 @@ class InnerFoldPanel(wx.Panel): @@ -315,390 +308,8 @@ class InnerFoldPanel(wx.Panel):
315 Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue()) 308 Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue())
316 309
317 310
318 -class Navigation():  
319 - def __init__(self, pedal_connection):  
320 - self.pedal_connection = pedal_connection  
321 -  
322 - self.image_fiducials = np.full([3, 3], np.nan)  
323 - self.correg = None  
324 - self.current_coord = 0, 0, 0  
325 - self.target = None  
326 - self.obj_reg = None  
327 - self.track_obj = False  
328 - self.m_change = None  
329 - self.all_fiducials = np.zeros((6, 6))  
330 -  
331 - self.event = threading.Event()  
332 - self.coord_queue = QueueCustom(maxsize=1)  
333 - self.icp_queue = QueueCustom(maxsize=1)  
334 - # self.visualization_queue = QueueCustom(maxsize=1)  
335 - self.serial_port_queue = QueueCustom(maxsize=1)  
336 - self.coord_tracts_queue = QueueCustom(maxsize=1)  
337 - self.tracts_queue = QueueCustom(maxsize=1)  
338 -  
339 - # Tracker parameters  
340 - self.ref_mode_id = const.DEFAULT_REF_MODE  
341 -  
342 - # Tractography parameters  
343 - self.trk_inp = None  
344 - self.trekker = None  
345 - self.n_threads = None  
346 - self.view_tracts = False  
347 - self.peel_loaded = False  
348 - self.enable_act = False  
349 - self.act_data = None  
350 - self.n_tracts = const.N_TRACTS  
351 - self.seed_offset = const.SEED_OFFSET  
352 - self.seed_radius = const.SEED_RADIUS  
353 - self.sleep_nav = const.SLEEP_NAVIGATION  
354 -  
355 - # Serial port  
356 - self.serial_port = None  
357 - self.serial_port_connection = None  
358 -  
359 - # During navigation  
360 - self.coil_at_target = False  
361 -  
362 - self.__bind_events()  
363 -  
364 - def __bind_events(self):  
365 - Publisher.subscribe(self.CoilAtTarget, 'Coil at target')  
366 -  
367 - def CoilAtTarget(self, state):  
368 - self.coil_at_target = state  
369 -  
370 - def UpdateSleep(self, sleep):  
371 - self.sleep_nav = sleep  
372 - self.serial_port_connection.sleep_nav = sleep  
373 -  
374 - def SerialPortEnabled(self):  
375 - return self.serial_port is not None  
376 -  
377 - def SetReferenceMode(self, value):  
378 - self.ref_mode_id = value  
379 -  
380 - def GetReferenceMode(self):  
381 - return self.ref_mode_id  
382 -  
383 - def SetImageFiducial(self, fiducial_index, coord):  
384 - self.image_fiducials[fiducial_index, :] = coord  
385 -  
386 - print("Set image fiducial {} to coordinates {}".format(fiducial_index, coord))  
387 -  
388 - def AreImageFiducialsSet(self):  
389 - return not np.isnan(self.image_fiducials).any()  
390 -  
391 - def UpdateFiducialRegistrationError(self, tracker):  
392 - tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()  
393 -  
394 - self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])  
395 -  
396 - self.fre = db.calculate_fre(tracker_fiducials_raw, self.all_fiducials, self.ref_mode_id, self.m_change)  
397 -  
398 - def GetFiducialRegistrationError(self, icp):  
399 - fre = icp.icp_fre if icp.use_icp else self.fre  
400 - return fre, fre <= const.FIDUCIAL_REGISTRATION_ERROR_THRESHOLD  
401 -  
402 - def PedalStateChanged(self, state):  
403 - if state is True and self.coil_at_target and self.SerialPortEnabled():  
404 - self.serial_port_connection.SendPulse()  
405 -  
406 - def StartNavigation(self, tracker):  
407 - tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()  
408 -  
409 - # initialize jobs list  
410 - jobs_list = []  
411 -  
412 - if self.event.is_set():  
413 - self.event.clear()  
414 -  
415 - vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded]  
416 - vis_queues = [self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue]  
417 -  
418 - Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components)  
419 -  
420 - self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])  
421 -  
422 - # fiducials matrix  
423 - m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T,  
424 - shear=False, scale=False)  
425 - self.m_change = m_change  
426 -  
427 - errors = False  
428 -  
429 - if self.track_obj:  
430 - # if object tracking is selected  
431 - if self.obj_reg is None:  
432 - # check if object registration was performed  
433 - wx.MessageBox(_("Perform coil registration before navigation."), _("InVesalius 3"))  
434 - errors = True  
435 - else:  
436 - # if object registration was correctly performed continue with navigation  
437 - # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix  
438 - obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg  
439 -  
440 - coreg_data = [m_change, obj_ref_mode]  
441 -  
442 - if self.ref_mode_id:  
443 - coord_raw = dco.GetCoordinates(tracker.trk_init, tracker.tracker_id, self.ref_mode_id)  
444 - else:  
445 - coord_raw = np.array([None])  
446 -  
447 - obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)  
448 - coreg_data.extend(obj_data)  
449 -  
450 - queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue]  
451 - jobs_list.append(dcr.CoordinateCorregistrate(self.ref_mode_id, tracker, coreg_data,  
452 - self.view_tracts, queues,  
453 - self.event, self.sleep_nav, tracker.tracker_id,  
454 - self.target))  
455 - else:  
456 - coreg_data = (m_change, 0)  
457 - queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue]  
458 - jobs_list.append(dcr.CoordinateCorregistrateNoObject(self.ref_mode_id, tracker, coreg_data,  
459 - self.view_tracts, queues,  
460 - self.event, self.sleep_nav))  
461 -  
462 - if not errors:  
463 - #TODO: Test the serial port thread  
464 - if self.SerialPortEnabled():  
465 - self.serial_port_connection = spc.SerialPortConnection(  
466 - self.serial_port,  
467 - self.serial_port_queue,  
468 - self.event,  
469 - self.sleep_nav,  
470 - )  
471 - self.serial_port_connection.Connect()  
472 - jobs_list.append(self.serial_port_connection)  
473 -  
474 - if self.view_tracts:  
475 - # initialize Trekker parameters  
476 - slic = sl.Slice()  
477 - prj_data = prj.Project()  
478 - matrix_shape = tuple(prj_data.matrix_shape)  
479 - affine = slic.affine.copy()  
480 - affine[1, -1] -= matrix_shape[1]  
481 - affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)  
482 - Publisher.sendMessage("Update marker offset state", create=True)  
483 - self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius,\  
484 - self.n_threads, self.act_data, affine_vtk, matrix_shape[1]  
485 - # print("Appending the tract computation thread!")  
486 - queues = [self.coord_tracts_queue, self.tracts_queue]  
487 - if self.enable_act:  
488 - jobs_list.append(dti.ComputeTractsACTThread(self.trk_inp, queues, self.event, self.sleep_nav))  
489 - else:  
490 - jobs_list.append(dti.ComputeTractsThread(self.trk_inp, queues, self.event, self.sleep_nav))  
491 -  
492 - jobs_list.append(UpdateNavigationScene(vis_queues, vis_components,  
493 - self.event, self.sleep_nav))  
494 -  
495 - for jobs in jobs_list:  
496 - # jobs.daemon = True  
497 - jobs.start()  
498 - # del jobs  
499 -  
500 - if self.pedal_connection is not None:  
501 - self.pedal_connection.add_callback('navigation', self.PedalStateChanged)  
502 -  
503 - def StopNavigation(self):  
504 - self.event.set()  
505 -  
506 - if self.pedal_connection is not None:  
507 - self.pedal_connection.remove_callback('navigation')  
508 -  
509 - self.coord_queue.clear()  
510 - self.coord_queue.join()  
511 -  
512 - if self.SerialPortEnabled():  
513 - self.serial_port_connection.join()  
514 -  
515 - self.serial_port_queue.clear()  
516 - self.serial_port_queue.join()  
517 -  
518 - if self.view_tracts:  
519 - self.coord_tracts_queue.clear()  
520 - self.coord_tracts_queue.join()  
521 -  
522 - self.tracts_queue.clear()  
523 - self.tracts_queue.join()  
524 -  
525 - vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded]  
526 - Publisher.sendMessage("Navigation status", nav_status=False, vis_status=vis_components)  
527 -  
528 -  
529 -class Tracker():  
530 - def __init__(self):  
531 - self.trk_init = None  
532 - self.tracker_id = const.DEFAULT_TRACKER  
533 -  
534 - self.tracker_fiducials = np.full([3, 3], np.nan)  
535 - self.tracker_fiducials_raw = np.zeros((6, 6))  
536 -  
537 - self.tracker_connected = False  
538 -  
539 - def SetTracker(self, new_tracker):  
540 - if new_tracker:  
541 - self.DisconnectTracker()  
542 -  
543 - self.trk_init = dt.TrackerConnection(new_tracker, None, 'connect')  
544 - if not self.trk_init[0]:  
545 - dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1])  
546 -  
547 - self.tracker_id = 0  
548 - self.tracker_connected = False  
549 - else:  
550 - self.tracker_id = new_tracker  
551 - self.tracker_connected = True  
552 -  
553 - def DisconnectTracker(self):  
554 - if self.tracker_connected:  
555 - self.ResetTrackerFiducials()  
556 - Publisher.sendMessage('Update status text in GUI',  
557 - label=_("Disconnecting tracker ..."))  
558 - Publisher.sendMessage('Remove sensors ID')  
559 - Publisher.sendMessage('Remove object data')  
560 - self.trk_init = dt.TrackerConnection(self.tracker_id, self.trk_init[0], 'disconnect')  
561 - if not self.trk_init[0]:  
562 - self.tracker_connected = False  
563 - self.tracker_id = 0  
564 -  
565 - Publisher.sendMessage('Update status text in GUI',  
566 - label=_("Tracker disconnected"))  
567 - print("Tracker disconnected!")  
568 - else:  
569 - Publisher.sendMessage('Update status text in GUI',  
570 - label=_("Tracker still connected"))  
571 - print("Tracker still connected!")  
572 -  
573 - def IsTrackerInitialized(self):  
574 - return self.trk_init and self.tracker_id and self.tracker_connected  
575 -  
576 - def AreTrackerFiducialsSet(self):  
577 - return not np.isnan(self.tracker_fiducials).any()  
578 -  
579 - def GetTrackerCoordinates(self, ref_mode_id, n_samples=1):  
580 - coord_raw_samples = {}  
581 - coord_samples = {}  
582 -  
583 - for i in range(n_samples):  
584 - coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, ref_mode_id)  
585 -  
586 - if ref_mode_id == const.DYNAMIC_REF:  
587 - coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :])  
588 - else:  
589 - coord = coord_raw[0, :]  
590 - coord[2] = -coord[2]  
591 -  
592 - coord_raw_samples[i] = coord_raw  
593 - coord_samples[i] = coord  
594 -  
595 - coord_raw_avg = np.median(list(coord_raw_samples.values()), axis=0)  
596 - coord_avg = np.median(list(coord_samples.values()), axis=0)  
597 -  
598 - return coord_avg, coord_raw_avg  
599 -  
600 - def SetTrackerFiducial(self, ref_mode_id, fiducial_index):  
601 - coord, coord_raw = self.GetTrackerCoordinates(  
602 - ref_mode_id=ref_mode_id,  
603 - n_samples=const.CALIBRATION_TRACKER_SAMPLES,  
604 - )  
605 -  
606 - # Update tracker fiducial with tracker coordinates  
607 - self.tracker_fiducials[fiducial_index, :] = coord[0:3]  
608 -  
609 - assert 0 <= fiducial_index <= 2, "Fiducial index out of range (0-2): {}".format(fiducial_index)  
610 -  
611 - self.tracker_fiducials_raw[2 * fiducial_index, :] = coord_raw[0, :]  
612 - self.tracker_fiducials_raw[2 * fiducial_index + 1, :] = coord_raw[1, :]  
613 -  
614 - print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3]))  
615 -  
616 - def ResetTrackerFiducials(self):  
617 - for m in range(3):  
618 - self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan]  
619 -  
620 - def GetTrackerFiducials(self):  
621 - return self.tracker_fiducials, self.tracker_fiducials_raw  
622 -  
623 - def GetTrackerInfo(self):  
624 - return self.trk_init, self.tracker_id  
625 -  
626 - def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre):  
627 - if self.tracker_connected:  
628 - selection_ctrl.SetSelection(self.tracker_id)  
629 - else:  
630 - selection_ctrl.SetSelection(0)  
631 -  
632 - # Update tracker location in the UI.  
633 - for m in range(3):  
634 - coord = self.tracker_fiducials[m, :]  
635 - for n in range(0, 3):  
636 - value = 0.0 if np.isnan(coord[n]) else float(coord[n])  
637 - numctrls_fiducial[m][n].SetValue(value)  
638 -  
639 - txtctrl_fre.SetValue('')  
640 - txtctrl_fre.SetBackgroundColour('WHITE')  
641 -  
642 - def get_trackers(self):  
643 - return const.TRACKERS  
644 -  
645 -class ICP():  
646 - def __init__(self):  
647 - self.use_icp = False  
648 - self.m_icp = None  
649 - self.icp_fre = None  
650 -  
651 - def StartICP(self, navigation, tracker):  
652 - ref_mode_id = navigation.GetReferenceMode()  
653 -  
654 - if not self.use_icp:  
655 - if dlg.ICPcorregistration(navigation.fre):  
656 - Publisher.sendMessage('Stop navigation')  
657 - use_icp, self.m_icp = self.OnICP(navigation, tracker, navigation.m_change)  
658 - if use_icp:  
659 - self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials,  
660 - ref_mode_id, navigation.m_change, self.m_icp)  
661 - self.SetICP(navigation, use_icp)  
662 - else:  
663 - print("ICP canceled")  
664 - Publisher.sendMessage('Start navigation')  
665 -  
666 - def OnICP(self, navigation, tracker, m_change):  
667 - ref_mode_id = navigation.GetReferenceMode()  
668 -  
669 - dialog = dlg.ICPCorregistrationDialog(nav_prop=(m_change, tracker.tracker_id, tracker.trk_init, ref_mode_id))  
670 -  
671 - if dialog.ShowModal() == wx.ID_OK:  
672 - m_icp, point_coord, transformed_points, prev_error, final_error = dialog.GetValue()  
673 - # TODO: checkbox in the dialog to transfer the icp points to 3D viewer  
674 - #create markers  
675 - # for i in range(len(point_coord)):  
676 - # img_coord = point_coord[i][0],-point_coord[i][1],point_coord[i][2], 0, 0, 0  
677 - # transf_coord = transformed_points[i][0],-transformed_points[i][1],transformed_points[i][2], 0, 0, 0  
678 - # Publisher.sendMessage('Create marker', coord=img_coord, marker_id=None, colour=(1,0,0))  
679 - # Publisher.sendMessage('Create marker', coord=transf_coord, marker_id=None, colour=(0,0,1))  
680 - if m_icp is not None:  
681 - dlg.ReportICPerror(prev_error, final_error)  
682 - use_icp = True  
683 - else:  
684 - use_icp = False  
685 -  
686 - return use_icp, m_icp  
687 -  
688 - else:  
689 - return self.use_icp, self.m_icp  
690 -  
691 - def SetICP(self, navigation, use_icp):  
692 - self.use_icp = use_icp  
693 - navigation.icp_queue.put_nowait([self.use_icp, self.m_icp])  
694 -  
695 - def ResetICP(self):  
696 - self.use_icp = False  
697 - self.m_icp = None  
698 - self.icp_fre = None  
699 -  
700 class NeuronavigationPanel(wx.Panel): 311 class NeuronavigationPanel(wx.Panel):
701 - def __init__(self, parent, tracker): 312 + def __init__(self, parent, tracker, pedal_connection):
702 wx.Panel.__init__(self, parent) 313 wx.Panel.__init__(self, parent)
703 try: 314 try:
704 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) 315 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
@@ -711,15 +322,16 @@ class NeuronavigationPanel(wx.Panel): @@ -711,15 +322,16 @@ class NeuronavigationPanel(wx.Panel):
711 self.__bind_events() 322 self.__bind_events()
712 323
713 # Initialize global variables 324 # Initialize global variables
714 - self.pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None 325 + self.pedal_connection = pedal_connection
715 self.navigation = Navigation( 326 self.navigation = Navigation(
716 - pedal_connection=self.pedal_connection, 327 + pedal_connection=pedal_connection,
717 ) 328 )
718 self.icp = ICP() 329 self.icp = ICP()
719 self.tracker = tracker 330 self.tracker = tracker
720 331
721 self.nav_status = False 332 self.nav_status = False
722 self.tracker_fiducial_being_set = None 333 self.tracker_fiducial_being_set = None
  334 + self.current_coord = 0, 0, 0
723 335
724 # Initialize list of buttons and numctrls for wx objects 336 # Initialize list of buttons and numctrls for wx objects
725 self.btns_set_fiducial = [None, None, None, None, None, None] 337 self.btns_set_fiducial = [None, None, None, None, None, None]
@@ -776,7 +388,7 @@ class NeuronavigationPanel(wx.Panel): @@ -776,7 +388,7 @@ class NeuronavigationPanel(wx.Panel):
776 txt_fre = wx.StaticText(self, -1, _('FRE:')) 388 txt_fre = wx.StaticText(self, -1, _('FRE:'))
777 txt_icp = wx.StaticText(self, -1, _('Refine:')) 389 txt_icp = wx.StaticText(self, -1, _('Refine:'))
778 390
779 - if self.pedal_connection is not None and self.pedal_connection.in_use: 391 + if pedal_connection is not None and pedal_connection.in_use:
780 txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) 392 txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:'))
781 else: 393 else:
782 txt_pedal_pressed = None 394 txt_pedal_pressed = None
@@ -805,14 +417,14 @@ class NeuronavigationPanel(wx.Panel): @@ -805,14 +417,14 @@ class NeuronavigationPanel(wx.Panel):
805 self.checkbox_icp = checkbox_icp 417 self.checkbox_icp = checkbox_icp
806 418
807 # An indicator for pedal trigger 419 # An indicator for pedal trigger
808 - if self.pedal_connection is not None and self.pedal_connection.in_use: 420 + if pedal_connection is not None and pedal_connection.in_use:
809 tooltip = wx.ToolTip(_(u"Is the pedal pressed")) 421 tooltip = wx.ToolTip(_(u"Is the pedal pressed"))
810 checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) 422 checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' '))
811 checkbox_pedal_pressed.SetValue(False) 423 checkbox_pedal_pressed.SetValue(False)
812 checkbox_pedal_pressed.Enable(False) 424 checkbox_pedal_pressed.Enable(False)
813 checkbox_pedal_pressed.SetToolTip(tooltip) 425 checkbox_pedal_pressed.SetToolTip(tooltip)
814 426
815 - self.pedal_connection.add_callback('gui', checkbox_pedal_pressed.SetValue) 427 + pedal_connection.add_callback('gui', checkbox_pedal_pressed.SetValue)
816 428
817 self.checkbox_pedal_pressed = checkbox_pedal_pressed 429 self.checkbox_pedal_pressed = checkbox_pedal_pressed
818 else: 430 else:
@@ -846,7 +458,7 @@ class NeuronavigationPanel(wx.Panel): @@ -846,7 +458,7 @@ class NeuronavigationPanel(wx.Panel):
846 (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) 458 (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
847 459
848 pedal_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) 460 pedal_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5)
849 - if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: 461 + if HAS_PEDAL_CONNECTION and pedal_connection.in_use:
850 pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), 462 pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL),
851 (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) 463 (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
852 464
@@ -982,7 +594,7 @@ class NeuronavigationPanel(wx.Panel): @@ -982,7 +594,7 @@ class NeuronavigationPanel(wx.Panel):
982 594
983 def UpdateImageCoordinates(self, position): 595 def UpdateImageCoordinates(self, position):
984 # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication. 596 # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication.
985 - self.navigation.current_coord = position 597 + self.current_coord = position
986 598
987 for m in [0, 1, 2]: 599 for m in [0, 1, 2]:
988 if not self.btns_set_fiducial[m].GetValue(): 600 if not self.btns_set_fiducial[m].GetValue():
@@ -1039,7 +651,7 @@ class NeuronavigationPanel(wx.Panel): @@ -1039,7 +651,7 @@ class NeuronavigationPanel(wx.Panel):
1039 fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] 651 fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name']
1040 652
1041 # XXX: This is still a bit hard to read, could be cleaned up. 653 # XXX: This is still a bit hard to read, could be cleaned up.
1042 - marker_id = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0] 654 + label = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0]
1043 655
1044 if self.btns_set_fiducial[n].GetValue(): 656 if self.btns_set_fiducial[n].GetValue():
1045 coord = self.numctrls_fiducial[n][0].GetValue(),\ 657 coord = self.numctrls_fiducial[n][0].GetValue(),\
@@ -1053,13 +665,13 @@ class NeuronavigationPanel(wx.Panel): @@ -1053,13 +665,13 @@ class NeuronavigationPanel(wx.Panel):
1053 seed = 3 * [0.] 665 seed = 3 * [0.]
1054 666
1055 Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size, 667 Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size,
1056 - marker_id=marker_id, seed=seed) 668 + marker_id=label, seed=seed)
1057 else: 669 else:
1058 for m in [0, 1, 2]: 670 for m in [0, 1, 2]:
1059 self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) 671 self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m]))
1060 672
1061 Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=np.nan) 673 Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=np.nan)
1062 - Publisher.sendMessage('Delete fiducial marker', marker_id=marker_id) 674 + Publisher.sendMessage('Delete fiducial marker', label=label)
1063 675
1064 def OnTrackerFiducials(self, n, evt, ctrl): 676 def OnTrackerFiducials(self, n, evt, ctrl):
1065 677
@@ -1074,14 +686,16 @@ class NeuronavigationPanel(wx.Panel): @@ -1074,14 +686,16 @@ class NeuronavigationPanel(wx.Panel):
1074 def set_fiducial_callback(): 686 def set_fiducial_callback():
1075 fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name'] 687 fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name']
1076 Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) 688 Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name)
1077 - self.pedal_connection.remove_callback('fiducial') 689 + if self.pedal_connection is not None:
  690 + self.pedal_connection.remove_callback('fiducial')
1078 691
1079 ctrl.SetValue(False) 692 ctrl.SetValue(False)
1080 self.tracker_fiducial_being_set = None 693 self.tracker_fiducial_being_set = None
1081 694
1082 if ctrl.GetValue(): 695 if ctrl.GetValue():
1083 self.tracker_fiducial_being_set = n 696 self.tracker_fiducial_being_set = n
1084 - self.pedal_connection.add_callback('fiducial', set_fiducial_callback) 697 + if self.pedal_connection is not None:
  698 + self.pedal_connection.add_callback('fiducial', set_fiducial_callback)
1085 else: 699 else:
1086 set_fiducial_callback() 700 set_fiducial_callback()
1087 701
@@ -1195,7 +809,7 @@ class NeuronavigationPanel(wx.Panel): @@ -1195,7 +809,7 @@ class NeuronavigationPanel(wx.Panel):
1195 809
1196 810
1197 class ObjectRegistrationPanel(wx.Panel): 811 class ObjectRegistrationPanel(wx.Panel):
1198 - def __init__(self, parent, tracker): 812 + def __init__(self, parent, tracker, pedal_connection):
1199 wx.Panel.__init__(self, parent) 813 wx.Panel.__init__(self, parent)
1200 try: 814 try:
1201 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) 815 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
@@ -1206,6 +820,7 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1206,6 +820,7 @@ class ObjectRegistrationPanel(wx.Panel):
1206 self.coil_list = const.COIL 820 self.coil_list = const.COIL
1207 821
1208 self.tracker = tracker 822 self.tracker = tracker
  823 + self.pedal_connection = pedal_connection
1209 824
1210 self.nav_prop = None 825 self.nav_prop = None
1211 self.obj_fiducials = None 826 self.obj_fiducials = None
@@ -1368,7 +983,7 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1368,7 +983,7 @@ class ObjectRegistrationPanel(wx.Panel):
1368 def OnLinkCreate(self, event=None): 983 def OnLinkCreate(self, event=None):
1369 984
1370 if self.tracker.IsTrackerInitialized(): 985 if self.tracker.IsTrackerInitialized():
1371 - dialog = dlg.ObjectCalibrationDialog(self.tracker) 986 + dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection)
1372 try: 987 try:
1373 if dialog.ShowModal() == wx.ID_OK: 988 if dialog.ShowModal() == wx.ID_OK:
1374 self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue() 989 self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue()
@@ -1479,7 +1094,6 @@ class MarkersPanel(wx.Panel): @@ -1479,7 +1094,6 @@ class MarkersPanel(wx.Panel):
1479 self.current_angle = 0, 0, 0 1094 self.current_angle = 0, 0, 0
1480 self.current_seed = 0, 0, 0 1095 self.current_seed = 0, 0, 0
1481 self.list_coord = [] 1096 self.list_coord = []
1482 - self.marker_ind = 0  
1483 self.tgt_flag = self.tgt_index = None 1097 self.tgt_flag = self.tgt_index = None
1484 self.nav_status = False 1098 self.nav_status = False
1485 1099
@@ -1525,7 +1139,7 @@ class MarkersPanel(wx.Panel): @@ -1525,7 +1139,7 @@ class MarkersPanel(wx.Panel):
1525 1139
1526 # Buttons to delete or remove markers 1140 # Buttons to delete or remove markers
1527 btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23)) 1141 btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23))
1528 - btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteSingleMarker) 1142 + btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteMultipleMarkers)
1529 1143
1530 btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23)) 1144 btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23))
1531 btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers) 1145 btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers)
@@ -1564,7 +1178,7 @@ class MarkersPanel(wx.Panel): @@ -1564,7 +1178,7 @@ class MarkersPanel(wx.Panel):
1564 def __bind_events(self): 1178 def __bind_events(self):
1565 # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points') 1179 # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points')
1566 Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point') 1180 Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point')
1567 - Publisher.subscribe(self.OnDeleteSingleMarker, 'Delete fiducial marker') 1181 + Publisher.subscribe(self.OnDeleteMultipleMarkers, 'Delete fiducial marker')
1568 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') 1182 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers')
1569 Publisher.subscribe(self.CreateMarker, 'Create marker') 1183 Publisher.subscribe(self.CreateMarker, 'Create marker')
1570 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') 1184 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
@@ -1680,7 +1294,6 @@ class MarkersPanel(wx.Panel): @@ -1680,7 +1294,6 @@ class MarkersPanel(wx.Panel):
1680 1294
1681 if result == wx.ID_OK: 1295 if result == wx.ID_OK:
1682 self.list_coord = [] 1296 self.list_coord = []
1683 - self.marker_ind = 0  
1684 Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount()) 1297 Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount())
1685 self.lc.DeleteAllItems() 1298 self.lc.DeleteAllItems()
1686 Publisher.sendMessage('Stop Blink Marker', index='DeleteAll') 1299 Publisher.sendMessage('Stop Blink Marker', index='DeleteAll')
@@ -1691,46 +1304,47 @@ class MarkersPanel(wx.Panel): @@ -1691,46 +1304,47 @@ class MarkersPanel(wx.Panel):
1691 if not hasattr(evt, 'data'): 1304 if not hasattr(evt, 'data'):
1692 wx.MessageBox(_("Target deleted."), _("InVesalius 3")) 1305 wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
1693 1306
1694 - def OnDeleteSingleMarker(self, evt=None, marker_id=None):  
1695 - # OnDeleteSingleMarker is used for both pubsub and button click events 1307 + def OnDeleteMultipleMarkers(self, evt=None, label=None):
  1308 + # OnDeleteMultipleMarkers is used for both pubsub and button click events
1696 # Pubsub is used for fiducial handle and button click for all others 1309 # Pubsub is used for fiducial handle and button click for all others
1697 1310
1698 - if not evt:  
1699 - if self.lc.GetItemCount(): 1311 + if not evt: # called through pubsub
  1312 + index = []
  1313 + allowed_labels = itertools.chain(*(const.BTNS_IMG_MARKERS[i].values() for i in const.BTNS_IMG_MARKERS))
  1314 +
  1315 + if label and (label in allowed_labels):
1700 for id_n in range(self.lc.GetItemCount()): 1316 for id_n in range(self.lc.GetItemCount()):
1701 item = self.lc.GetItem(id_n, 4) 1317 item = self.lc.GetItem(id_n, 4)
1702 - if item.GetText() == marker_id:  
1703 - for i in const.BTNS_IMG_MARKERS:  
1704 - if marker_id in list(const.BTNS_IMG_MARKERS[i].values())[0]:  
1705 - self.lc.Focus(item.GetId())  
1706 - index = [self.lc.GetFocusedItem()]  
1707 - else:  
1708 - if self.lc.GetFirstSelected() != -1:  
1709 - index = self.GetSelectedItems()  
1710 - else:  
1711 - index = None 1318 + if item.GetText() == label:
  1319 + self.lc.Focus(item.GetId())
  1320 + index = [self.lc.GetFocusedItem()]
  1321 +
  1322 + else: # called from button click
  1323 + index = self.__getSelectedItems()
1712 1324
1713 - #TODO: There are bugs when no marker is selected, test and improve 1325 + #TODO: Bug - when deleting multiple markers and target is not the first marker
1714 if index: 1326 if index:
1715 if self.tgt_flag and self.tgt_index == index[0]: 1327 if self.tgt_flag and self.tgt_index == index[0]:
1716 self.tgt_flag = self.tgt_index = None 1328 self.tgt_flag = self.tgt_index = None
1717 Publisher.sendMessage('Disable or enable coil tracker', status=False) 1329 Publisher.sendMessage('Disable or enable coil tracker', status=False)
1718 - wx.MessageBox(_("No data selected."), _("InVesalius 3")) 1330 + wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
1719 1331
1720 - self.DeleteMarker(index) 1332 + self.__deleteMultipleMarkers(index)
1721 else: 1333 else:
1722 - wx.MessageBox(_("Target deleted."), _("InVesalius 3")) 1334 + wx.MessageBox(_("No data selected."), _("InVesalius 3"))
1723 1335
1724 - def DeleteMarker(self, index): 1336 + def __deleteMultipleMarkers(self, index):
  1337 + """ Delete multiple markers indexed by index. index must be sorted in
  1338 + the ascending order.
  1339 + """
1725 for i in reversed(index): 1340 for i in reversed(index):
1726 del self.list_coord[i] 1341 del self.list_coord[i]
1727 self.lc.DeleteItem(i) 1342 self.lc.DeleteItem(i)
1728 for n in range(0, self.lc.GetItemCount()): 1343 for n in range(0, self.lc.GetItemCount()):
1729 self.lc.SetItem(n, 0, str(n+1)) 1344 self.lc.SetItem(n, 0, str(n+1))
1730 - self.marker_ind -= 1  
1731 Publisher.sendMessage('Remove marker', index=index) 1345 Publisher.sendMessage('Remove marker', index=index)
1732 1346
1733 - def OnCreateMarker(self, evt=None, coord=None, marker_id=None, colour=None): 1347 + def OnCreateMarker(self, evt):
1734 self.CreateMarker() 1348 self.CreateMarker()
1735 1349
1736 def OnLoadMarkers(self, evt): 1350 def OnLoadMarkers(self, evt):
@@ -1792,7 +1406,7 @@ class MarkersPanel(wx.Panel): @@ -1792,7 +1406,7 @@ class MarkersPanel(wx.Panel):
1792 marker_id = '*' 1406 marker_id = '*'
1793 1407
1794 self.CreateMarker(coord=coord, colour=colour, size=size, 1408 self.CreateMarker(coord=coord, colour=colour, size=size,
1795 - marker_id=marker_id, target_id=target_id, seed=seed) 1409 + label=marker_id, target_id=target_id, seed=seed)
1796 1410
1797 # if there are multiple TARGETS will set the last one 1411 # if there are multiple TARGETS will set the last one
1798 if target: 1412 if target:
@@ -1847,7 +1461,7 @@ class MarkersPanel(wx.Panel): @@ -1847,7 +1461,7 @@ class MarkersPanel(wx.Panel):
1847 target_id = line[20] 1461 target_id = line[20]
1848 1462
1849 self.CreateMarker(coord=coord, colour=colour, size=size, 1463 self.CreateMarker(coord=coord, colour=colour, size=size,
1850 - marker_id=marker_id, target_id=target_id, seed=seed) 1464 + label=marker_id, target_id=target_id, seed=seed)
1851 1465
1852 # if there are multiple TARGETS will set the last one 1466 # if there are multiple TARGETS will set the last one
1853 if target: 1467 if target:
@@ -1903,7 +1517,7 @@ class MarkersPanel(wx.Panel): @@ -1903,7 +1517,7 @@ class MarkersPanel(wx.Panel):
1903 def OnSelectSize(self, evt, ctrl): 1517 def OnSelectSize(self, evt, ctrl):
1904 self.marker_size = ctrl.GetValue() 1518 self.marker_size = ctrl.GetValue()
1905 1519
1906 - def CreateMarker(self, coord=None, colour=None, size=None, marker_id='*', target_id='*', seed=None): 1520 + def CreateMarker(self, coord=None, colour=None, size=None, label='*', target_id='*', seed=None):
1907 coord = coord or self.current_coord 1521 coord = coord or self.current_coord
1908 colour = colour or self.marker_colour 1522 colour = colour or self.marker_colour
1909 size = size or self.marker_size 1523 size = size or self.marker_size
@@ -1917,9 +1531,7 @@ class MarkersPanel(wx.Panel): @@ -1917,9 +1531,7 @@ class MarkersPanel(wx.Panel):
1917 # TODO: Use matrix coordinates and not world coordinates as current method. 1531 # TODO: Use matrix coordinates and not world coordinates as current method.
1918 # This makes easier for inter-software comprehension. 1532 # This makes easier for inter-software comprehension.
1919 1533
1920 - Publisher.sendMessage('Add marker', ball_id=self.marker_ind, size=size, colour=colour, coord=coord[0:3])  
1921 -  
1922 - self.marker_ind += 1 1534 + Publisher.sendMessage('Add marker', ball_id=len(self.list_coord), size=size, colour=colour, coord=coord[0:3])
1923 1535
1924 # List of lists with coordinates and properties of a marker 1536 # List of lists with coordinates and properties of a marker
1925 line = [] 1537 line = []
@@ -1928,7 +1540,7 @@ class MarkersPanel(wx.Panel): @@ -1928,7 +1540,7 @@ class MarkersPanel(wx.Panel):
1928 line.extend(orientation_world) 1540 line.extend(orientation_world)
1929 line.extend(colour) 1541 line.extend(colour)
1930 line.append(size) 1542 line.append(size)
1931 - line.append(marker_id) 1543 + line.append(label)
1932 line.extend(seed) 1544 line.extend(seed)
1933 line.append(target_id) 1545 line.append(target_id)
1934 1546
@@ -1944,21 +1556,24 @@ class MarkersPanel(wx.Panel): @@ -1944,21 +1556,24 @@ class MarkersPanel(wx.Panel):
1944 self.lc.SetItem(num_items, 1, str(round(coord[0], 2))) 1556 self.lc.SetItem(num_items, 1, str(round(coord[0], 2)))
1945 self.lc.SetItem(num_items, 2, str(round(coord[1], 2))) 1557 self.lc.SetItem(num_items, 2, str(round(coord[1], 2)))
1946 self.lc.SetItem(num_items, 3, str(round(coord[2], 2))) 1558 self.lc.SetItem(num_items, 3, str(round(coord[2], 2)))
1947 - self.lc.SetItem(num_items, 4, str(marker_id)) 1559 + self.lc.SetItem(num_items, 4, str(label))
1948 self.lc.EnsureVisible(num_items) 1560 self.lc.EnsureVisible(num_items)
1949 1561
1950 - def GetSelectedItems(self): 1562 + def __getSelectedItems(self):
1951 """ 1563 """
1952 - Returns a list of the selected items in the list control. 1564 + Returns a (possibly empty) list of the selected items in the list control.
1953 """ 1565 """
1954 selection = [] 1566 selection = []
1955 - index = self.lc.GetFirstSelected()  
1956 - selection.append(index)  
1957 - while len(selection) != self.lc.GetSelectedItemCount():  
1958 - index = self.lc.GetNextSelected(index)  
1959 - selection.append(index) 1567 +
  1568 + next = self.lc.GetFirstSelected()
  1569 +
  1570 + while next != -1:
  1571 + selection.append(next)
  1572 + next = self.lc.GetNextSelected(next)
  1573 +
1960 return selection 1574 return selection
1961 1575
  1576 +
1962 class DbsPanel(wx.Panel): 1577 class DbsPanel(wx.Panel):
1963 def __init__(self, parent): 1578 def __init__(self, parent):
1964 wx.Panel.__init__(self, parent) 1579 wx.Panel.__init__(self, parent)
@@ -2410,100 +2025,6 @@ class TractographyPanel(wx.Panel): @@ -2410,100 +2025,6 @@ class TractographyPanel(wx.Panel):
2410 Publisher.sendMessage('Remove tracts') 2025 Publisher.sendMessage('Remove tracts')
2411 2026
2412 2027
2413 -class QueueCustom(queue.Queue):  
2414 - """  
2415 - A custom queue subclass that provides a :meth:`clear` method.  
2416 - https://stackoverflow.com/questions/6517953/clear-all-items-from-the-queue  
2417 - Modified to a LIFO Queue type (Last-in-first-out). Seems to make sense for the navigation  
2418 - threads, as the last added coordinate should be the first to be processed.  
2419 - In the first tests in a short run, seems to increase the coord queue size considerably,  
2420 - possibly limiting the queue size is good.  
2421 - """  
2422 -  
2423 - def clear(self):  
2424 - """  
2425 - Clears all items from the queue.  
2426 - """  
2427 -  
2428 - with self.mutex:  
2429 - unfinished = self.unfinished_tasks - len(self.queue)  
2430 - if unfinished <= 0:  
2431 - if unfinished < 0:  
2432 - raise ValueError('task_done() called too many times')  
2433 - self.all_tasks_done.notify_all()  
2434 - self.unfinished_tasks = unfinished  
2435 - self.queue.clear()  
2436 - self.not_full.notify_all()  
2437 -  
2438 -  
2439 -class UpdateNavigationScene(threading.Thread):  
2440 -  
2441 - def __init__(self, vis_queues, vis_components, event, sle):  
2442 - """Class (threading) to update the navigation scene with all graphical elements.  
2443 -  
2444 - Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation  
2445 -  
2446 - :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene  
2447 - :type affine_vtk: vtkMatrix4x4  
2448 - :param visualization_queue: Queue instance that manage coordinates to be visualized  
2449 - :type visualization_queue: queue.Queue  
2450 - :param event: Threading event to coordinate when tasks as done and allow UI release  
2451 - :type event: threading.Event  
2452 - :param sle: Sleep pause in seconds  
2453 - :type sle: float  
2454 - """  
2455 -  
2456 - threading.Thread.__init__(self, name='UpdateScene')  
2457 - self.serial_port_enabled, self.view_tracts, self.peel_loaded = vis_components  
2458 - self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue = vis_queues  
2459 - self.sle = sle  
2460 - self.event = event  
2461 -  
2462 - def run(self):  
2463 - # count = 0  
2464 - while not self.event.is_set():  
2465 - got_coords = False  
2466 - try:  
2467 - coord, m_img, view_obj = self.coord_queue.get_nowait()  
2468 - got_coords = True  
2469 -  
2470 - # print('UpdateScene: get {}'.format(count))  
2471 -  
2472 - # use of CallAfter is mandatory otherwise crashes the wx interface  
2473 - if self.view_tracts:  
2474 - bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait()  
2475 - #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command  
2476 - wx.CallAfter(Publisher.sendMessage, 'Remove tracts')  
2477 - wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle,  
2478 - affine_vtk=affine_vtk, coord_offset=coord_offset)  
2479 - # wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset)  
2480 - self.tracts_queue.task_done()  
2481 -  
2482 - if self.serial_port_enabled:  
2483 - trigger_on = self.serial_port_queue.get_nowait()  
2484 - if trigger_on:  
2485 - wx.CallAfter(Publisher.sendMessage, 'Create marker')  
2486 - self.serial_port_queue.task_done()  
2487 -  
2488 - #TODO: If using the view_tracts substitute the raw coord from the offset coordinate, so the user  
2489 - # see the red cross in the position of the offset marker  
2490 - wx.CallAfter(Publisher.sendMessage, 'Update slices position', position=coord[:3])  
2491 - wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord)  
2492 - wx.CallAfter(Publisher.sendMessage, 'Update slice viewer')  
2493 -  
2494 - if view_obj:  
2495 - wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)  
2496 - wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix',m_img=m_img, coord=coord, flag= self.peel_loaded)  
2497 - self.coord_queue.task_done()  
2498 - # print('UpdateScene: done {}'.format(count))  
2499 - # count += 1  
2500 -  
2501 - sleep(self.sle)  
2502 - except queue.Empty:  
2503 - if got_coords:  
2504 - self.coord_queue.task_done()  
2505 -  
2506 -  
2507 class InputAttributes(object): 2028 class InputAttributes(object):
2508 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python 2029 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
2509 def __init__(self, *initial_data, **kwargs): 2030 def __init__(self, *initial_data, **kwargs):
invesalius/navigation/icp.py 0 → 100644
@@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
  1 +#--------------------------------------------------------------------------
  2 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  3 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  4 +# Homepage: http://www.softwarepublico.gov.br
  5 +# Contact: invesalius@cti.gov.br
  6 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  7 +#--------------------------------------------------------------------------
  8 +# Este programa e software livre; voce pode redistribui-lo e/ou
  9 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  10 +# publicada pela Free Software Foundation; de acordo com a versao 2
  11 +# da Licenca.
  12 +#
  13 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  14 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  15 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  16 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  17 +# detalhes.
  18 +#--------------------------------------------------------------------------
  19 +
  20 +import wx
  21 +
  22 +import invesalius.data.bases as db
  23 +import invesalius.gui.dialogs as dlg
  24 +from invesalius.pubsub import pub as Publisher
  25 +
  26 +
  27 +class ICP():
  28 + def __init__(self):
  29 + self.use_icp = False
  30 + self.m_icp = None
  31 + self.icp_fre = None
  32 +
  33 + def StartICP(self, navigation, tracker):
  34 + ref_mode_id = navigation.GetReferenceMode()
  35 +
  36 + if not self.use_icp:
  37 + if dlg.ICPcorregistration(navigation.fre):
  38 + Publisher.sendMessage('Stop navigation')
  39 + use_icp, self.m_icp = self.OnICP(navigation, tracker, navigation.m_change)
  40 + if use_icp:
  41 + self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials,
  42 + ref_mode_id, navigation.m_change, self.m_icp)
  43 + self.SetICP(navigation, use_icp)
  44 + else:
  45 + print("ICP canceled")
  46 + Publisher.sendMessage('Start navigation')
  47 +
  48 + def OnICP(self, navigation, tracker, m_change):
  49 + ref_mode_id = navigation.GetReferenceMode()
  50 +
  51 + dialog = dlg.ICPCorregistrationDialog(nav_prop=(m_change, tracker.tracker_id, tracker.trk_init, ref_mode_id))
  52 +
  53 + if dialog.ShowModal() == wx.ID_OK:
  54 + m_icp, point_coord, transformed_points, prev_error, final_error = dialog.GetValue()
  55 + # TODO: checkbox in the dialog to transfer the icp points to 3D viewer
  56 + #create markers
  57 + # for i in range(len(point_coord)):
  58 + # img_coord = point_coord[i][0],-point_coord[i][1],point_coord[i][2], 0, 0, 0
  59 + # transf_coord = transformed_points[i][0],-transformed_points[i][1],transformed_points[i][2], 0, 0, 0
  60 + # Publisher.sendMessage('Create marker', coord=img_coord, marker_id=None, colour=(1,0,0))
  61 + # Publisher.sendMessage('Create marker', coord=transf_coord, marker_id=None, colour=(0,0,1))
  62 + if m_icp is not None:
  63 + dlg.ReportICPerror(prev_error, final_error)
  64 + use_icp = True
  65 + else:
  66 + use_icp = False
  67 +
  68 + return use_icp, m_icp
  69 +
  70 + else:
  71 + return self.use_icp, self.m_icp
  72 +
  73 + def SetICP(self, navigation, use_icp):
  74 + self.use_icp = use_icp
  75 + navigation.icp_queue.put_nowait([self.use_icp, self.m_icp])
  76 +
  77 + def ResetICP(self):
  78 + self.use_icp = False
  79 + self.m_icp = None
  80 + self.icp_fre = None
invesalius/navigation/navigation.py 0 → 100644
@@ -0,0 +1,341 @@ @@ -0,0 +1,341 @@
  1 +#--------------------------------------------------------------------------
  2 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  3 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  4 +# Homepage: http://www.softwarepublico.gov.br
  5 +# Contact: invesalius@cti.gov.br
  6 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  7 +#--------------------------------------------------------------------------
  8 +# Este programa e software livre; voce pode redistribui-lo e/ou
  9 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  10 +# publicada pela Free Software Foundation; de acordo com a versao 2
  11 +# da Licenca.
  12 +#
  13 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  14 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  15 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  16 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  17 +# detalhes.
  18 +#--------------------------------------------------------------------------
  19 +
  20 +import threading
  21 +import queue
  22 +from time import sleep
  23 +
  24 +import wx
  25 +import numpy as np
  26 +
  27 +import invesalius.constants as const
  28 +import invesalius.project as prj
  29 +import invesalius.data.bases as db
  30 +import invesalius.data.coordinates as dco
  31 +import invesalius.data.coregistration as dcr
  32 +import invesalius.data.serial_port_connection as spc
  33 +import invesalius.data.slice_ as sl
  34 +import invesalius.data.tractography as dti
  35 +import invesalius.data.transformations as tr
  36 +import invesalius.data.vtk_utils as vtk_utils
  37 +from invesalius.pubsub import pub as Publisher
  38 +
  39 +
  40 +class QueueCustom(queue.Queue):
  41 + """
  42 + A custom queue subclass that provides a :meth:`clear` method.
  43 + https://stackoverflow.com/questions/6517953/clear-all-items-from-the-queue
  44 + Modified to a LIFO Queue type (Last-in-first-out). Seems to make sense for the navigation
  45 + threads, as the last added coordinate should be the first to be processed.
  46 + In the first tests in a short run, seems to increase the coord queue size considerably,
  47 + possibly limiting the queue size is good.
  48 + """
  49 +
  50 + def clear(self):
  51 + """
  52 + Clears all items from the queue.
  53 + """
  54 +
  55 + with self.mutex:
  56 + unfinished = self.unfinished_tasks - len(self.queue)
  57 + if unfinished <= 0:
  58 + if unfinished < 0:
  59 + raise ValueError('task_done() called too many times')
  60 + self.all_tasks_done.notify_all()
  61 + self.unfinished_tasks = unfinished
  62 + self.queue.clear()
  63 + self.not_full.notify_all()
  64 +
  65 +
  66 +class UpdateNavigationScene(threading.Thread):
  67 +
  68 + def __init__(self, vis_queues, vis_components, event, sle):
  69 + """Class (threading) to update the navigation scene with all graphical elements.
  70 +
  71 + Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation
  72 +
  73 + :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene
  74 + :type affine_vtk: vtkMatrix4x4
  75 + :param visualization_queue: Queue instance that manage coordinates to be visualized
  76 + :type visualization_queue: queue.Queue
  77 + :param event: Threading event to coordinate when tasks as done and allow UI release
  78 + :type event: threading.Event
  79 + :param sle: Sleep pause in seconds
  80 + :type sle: float
  81 + """
  82 +
  83 + threading.Thread.__init__(self, name='UpdateScene')
  84 + self.serial_port_enabled, self.view_tracts, self.peel_loaded = vis_components
  85 + self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue = vis_queues
  86 + self.sle = sle
  87 + self.event = event
  88 +
  89 + def run(self):
  90 + # count = 0
  91 + while not self.event.is_set():
  92 + got_coords = False
  93 + try:
  94 + coord, m_img, view_obj = self.coord_queue.get_nowait()
  95 + got_coords = True
  96 +
  97 + # print('UpdateScene: get {}'.format(count))
  98 +
  99 + # use of CallAfter is mandatory otherwise crashes the wx interface
  100 + if self.view_tracts:
  101 + bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait()
  102 + #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command
  103 + wx.CallAfter(Publisher.sendMessage, 'Remove tracts')
  104 + wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle,
  105 + affine_vtk=affine_vtk, coord_offset=coord_offset)
  106 + # wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset)
  107 + self.tracts_queue.task_done()
  108 +
  109 + if self.serial_port_enabled:
  110 + trigger_on = self.serial_port_queue.get_nowait()
  111 + if trigger_on:
  112 + wx.CallAfter(Publisher.sendMessage, 'Create marker')
  113 + self.serial_port_queue.task_done()
  114 +
  115 + #TODO: If using the view_tracts substitute the raw coord from the offset coordinate, so the user
  116 + # see the red cross in the position of the offset marker
  117 + wx.CallAfter(Publisher.sendMessage, 'Update slices position', position=coord[:3])
  118 + wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord)
  119 + wx.CallAfter(Publisher.sendMessage, 'Update slice viewer')
  120 +
  121 + if view_obj:
  122 + wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)
  123 + wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix',m_img=m_img, coord=coord, flag= self.peel_loaded)
  124 + self.coord_queue.task_done()
  125 + # print('UpdateScene: done {}'.format(count))
  126 + # count += 1
  127 +
  128 + sleep(self.sle)
  129 + except queue.Empty:
  130 + if got_coords:
  131 + self.coord_queue.task_done()
  132 +
  133 +
  134 +class Navigation():
  135 + def __init__(self, pedal_connection):
  136 + self.pedal_connection = pedal_connection
  137 +
  138 + self.image_fiducials = np.full([3, 3], np.nan)
  139 + self.correg = None
  140 + self.target = None
  141 + self.obj_reg = None
  142 + self.track_obj = False
  143 + self.m_change = None
  144 + self.all_fiducials = np.zeros((6, 6))
  145 +
  146 + self.event = threading.Event()
  147 + self.coord_queue = QueueCustom(maxsize=1)
  148 + self.icp_queue = QueueCustom(maxsize=1)
  149 + # self.visualization_queue = QueueCustom(maxsize=1)
  150 + self.serial_port_queue = QueueCustom(maxsize=1)
  151 + self.coord_tracts_queue = QueueCustom(maxsize=1)
  152 + self.tracts_queue = QueueCustom(maxsize=1)
  153 +
  154 + # Tracker parameters
  155 + self.ref_mode_id = const.DEFAULT_REF_MODE
  156 +
  157 + # Tractography parameters
  158 + self.trk_inp = None
  159 + self.trekker = None
  160 + self.n_threads = None
  161 + self.view_tracts = False
  162 + self.peel_loaded = False
  163 + self.enable_act = False
  164 + self.act_data = None
  165 + self.n_tracts = const.N_TRACTS
  166 + self.seed_offset = const.SEED_OFFSET
  167 + self.seed_radius = const.SEED_RADIUS
  168 + self.sleep_nav = const.SLEEP_NAVIGATION
  169 +
  170 + # Serial port
  171 + self.serial_port = None
  172 + self.serial_port_connection = None
  173 +
  174 + # During navigation
  175 + self.coil_at_target = False
  176 +
  177 + self.__bind_events()
  178 +
  179 + def __bind_events(self):
  180 + Publisher.subscribe(self.CoilAtTarget, 'Coil at target')
  181 +
  182 + def CoilAtTarget(self, state):
  183 + self.coil_at_target = state
  184 +
  185 + def UpdateSleep(self, sleep):
  186 + self.sleep_nav = sleep
  187 + self.serial_port_connection.sleep_nav = sleep
  188 +
  189 + def SerialPortEnabled(self):
  190 + return self.serial_port is not None
  191 +
  192 + def SetReferenceMode(self, value):
  193 + self.ref_mode_id = value
  194 +
  195 + def GetReferenceMode(self):
  196 + return self.ref_mode_id
  197 +
  198 + def SetImageFiducial(self, fiducial_index, coord):
  199 + self.image_fiducials[fiducial_index, :] = coord
  200 +
  201 + print("Set image fiducial {} to coordinates {}".format(fiducial_index, coord))
  202 +
  203 + def AreImageFiducialsSet(self):
  204 + return not np.isnan(self.image_fiducials).any()
  205 +
  206 + def UpdateFiducialRegistrationError(self, tracker):
  207 + tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()
  208 +
  209 + self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])
  210 +
  211 + self.fre = db.calculate_fre(tracker_fiducials_raw, self.all_fiducials, self.ref_mode_id, self.m_change)
  212 +
  213 + def GetFiducialRegistrationError(self, icp):
  214 + fre = icp.icp_fre if icp.use_icp else self.fre
  215 + return fre, fre <= const.FIDUCIAL_REGISTRATION_ERROR_THRESHOLD
  216 +
  217 + def PedalStateChanged(self, state):
  218 + if state is True and self.coil_at_target and self.SerialPortEnabled():
  219 + self.serial_port_connection.SendPulse()
  220 +
  221 + def StartNavigation(self, tracker):
  222 + tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()
  223 +
  224 + # initialize jobs list
  225 + jobs_list = []
  226 +
  227 + if self.event.is_set():
  228 + self.event.clear()
  229 +
  230 + vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded]
  231 + vis_queues = [self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue]
  232 +
  233 + Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components)
  234 +
  235 + self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])
  236 +
  237 + # fiducials matrix
  238 + m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T,
  239 + shear=False, scale=False)
  240 + self.m_change = m_change
  241 +
  242 + errors = False
  243 +
  244 + if self.track_obj:
  245 + # if object tracking is selected
  246 + if self.obj_reg is None:
  247 + # check if object registration was performed
  248 + wx.MessageBox(_("Perform coil registration before navigation."), _("InVesalius 3"))
  249 + errors = True
  250 + else:
  251 + # if object registration was correctly performed continue with navigation
  252 + # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix
  253 + obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg
  254 +
  255 + coreg_data = [m_change, obj_ref_mode]
  256 +
  257 + if self.ref_mode_id:
  258 + coord_raw = dco.GetCoordinates(tracker.trk_init, tracker.tracker_id, self.ref_mode_id)
  259 + else:
  260 + coord_raw = np.array([None])
  261 +
  262 + obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)
  263 + coreg_data.extend(obj_data)
  264 +
  265 + queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue]
  266 + jobs_list.append(dcr.CoordinateCorregistrate(self.ref_mode_id, tracker, coreg_data,
  267 + self.view_tracts, queues,
  268 + self.event, self.sleep_nav, tracker.tracker_id,
  269 + self.target))
  270 + else:
  271 + coreg_data = (m_change, 0)
  272 + queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue]
  273 + jobs_list.append(dcr.CoordinateCorregistrateNoObject(self.ref_mode_id, tracker, coreg_data,
  274 + self.view_tracts, queues,
  275 + self.event, self.sleep_nav))
  276 +
  277 + if not errors:
  278 + #TODO: Test the serial port thread
  279 + if self.SerialPortEnabled():
  280 + self.serial_port_connection = spc.SerialPortConnection(
  281 + self.serial_port,
  282 + self.serial_port_queue,
  283 + self.event,
  284 + self.sleep_nav,
  285 + )
  286 + self.serial_port_connection.Connect()
  287 + jobs_list.append(self.serial_port_connection)
  288 +
  289 + if self.view_tracts:
  290 + # initialize Trekker parameters
  291 + slic = sl.Slice()
  292 + prj_data = prj.Project()
  293 + matrix_shape = tuple(prj_data.matrix_shape)
  294 + affine = slic.affine.copy()
  295 + affine[1, -1] -= matrix_shape[1]
  296 + affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)
  297 + Publisher.sendMessage("Update marker offset state", create=True)
  298 + self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius,\
  299 + self.n_threads, self.act_data, affine_vtk, matrix_shape[1]
  300 + # print("Appending the tract computation thread!")
  301 + queues = [self.coord_tracts_queue, self.tracts_queue]
  302 + if self.enable_act:
  303 + jobs_list.append(dti.ComputeTractsACTThread(self.trk_inp, queues, self.event, self.sleep_nav))
  304 + else:
  305 + jobs_list.append(dti.ComputeTractsThread(self.trk_inp, queues, self.event, self.sleep_nav))
  306 +
  307 + jobs_list.append(UpdateNavigationScene(vis_queues, vis_components,
  308 + self.event, self.sleep_nav))
  309 +
  310 + for jobs in jobs_list:
  311 + # jobs.daemon = True
  312 + jobs.start()
  313 + # del jobs
  314 +
  315 + if self.pedal_connection is not None:
  316 + self.pedal_connection.add_callback('navigation', self.PedalStateChanged)
  317 +
  318 + def StopNavigation(self):
  319 + self.event.set()
  320 +
  321 + if self.pedal_connection is not None:
  322 + self.pedal_connection.remove_callback('navigation')
  323 +
  324 + self.coord_queue.clear()
  325 + self.coord_queue.join()
  326 +
  327 + if self.SerialPortEnabled():
  328 + self.serial_port_connection.join()
  329 +
  330 + self.serial_port_queue.clear()
  331 + self.serial_port_queue.join()
  332 +
  333 + if self.view_tracts:
  334 + self.coord_tracts_queue.clear()
  335 + self.coord_tracts_queue.join()
  336 +
  337 + self.tracts_queue.clear()
  338 + self.tracts_queue.join()
  339 +
  340 + vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded]
  341 + Publisher.sendMessage("Navigation status", nav_status=False, vis_status=vis_components)
invesalius/navigation/tracker.py 0 → 100644
@@ -0,0 +1,143 @@ @@ -0,0 +1,143 @@
  1 +#--------------------------------------------------------------------------
  2 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  3 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  4 +# Homepage: http://www.softwarepublico.gov.br
  5 +# Contact: invesalius@cti.gov.br
  6 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  7 +#--------------------------------------------------------------------------
  8 +# Este programa e software livre; voce pode redistribui-lo e/ou
  9 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  10 +# publicada pela Free Software Foundation; de acordo com a versao 2
  11 +# da Licenca.
  12 +#
  13 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  14 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  15 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  16 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  17 +# detalhes.
  18 +#--------------------------------------------------------------------------
  19 +
  20 +import numpy as np
  21 +
  22 +import invesalius.constants as const
  23 +import invesalius.data.coordinates as dco
  24 +import invesalius.data.trackers as dt
  25 +import invesalius.gui.dialogs as dlg
  26 +from invesalius.pubsub import pub as Publisher
  27 +
  28 +
  29 +class Tracker():
  30 + def __init__(self):
  31 + self.trk_init = None
  32 + self.tracker_id = const.DEFAULT_TRACKER
  33 +
  34 + self.tracker_fiducials = np.full([3, 3], np.nan)
  35 + self.tracker_fiducials_raw = np.zeros((6, 6))
  36 +
  37 + self.tracker_connected = False
  38 +
  39 + def SetTracker(self, new_tracker):
  40 + if new_tracker:
  41 + self.DisconnectTracker()
  42 +
  43 + self.trk_init = dt.TrackerConnection(new_tracker, None, 'connect')
  44 + if not self.trk_init[0]:
  45 + dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1])
  46 +
  47 + self.tracker_id = 0
  48 + self.tracker_connected = False
  49 + else:
  50 + self.tracker_id = new_tracker
  51 + self.tracker_connected = True
  52 +
  53 + def DisconnectTracker(self):
  54 + if self.tracker_connected:
  55 + self.ResetTrackerFiducials()
  56 + Publisher.sendMessage('Update status text in GUI',
  57 + label=_("Disconnecting tracker ..."))
  58 + Publisher.sendMessage('Remove sensors ID')
  59 + Publisher.sendMessage('Remove object data')
  60 + self.trk_init = dt.TrackerConnection(self.tracker_id, self.trk_init[0], 'disconnect')
  61 + if not self.trk_init[0]:
  62 + self.tracker_connected = False
  63 + self.tracker_id = 0
  64 +
  65 + Publisher.sendMessage('Update status text in GUI',
  66 + label=_("Tracker disconnected"))
  67 + print("Tracker disconnected!")
  68 + else:
  69 + Publisher.sendMessage('Update status text in GUI',
  70 + label=_("Tracker still connected"))
  71 + print("Tracker still connected!")
  72 +
  73 + def IsTrackerInitialized(self):
  74 + return self.trk_init and self.tracker_id and self.tracker_connected
  75 +
  76 + def AreTrackerFiducialsSet(self):
  77 + return not np.isnan(self.tracker_fiducials).any()
  78 +
  79 + def GetTrackerCoordinates(self, ref_mode_id, n_samples=1):
  80 + coord_raw_samples = {}
  81 + coord_samples = {}
  82 +
  83 + for i in range(n_samples):
  84 + coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, ref_mode_id)
  85 +
  86 + if ref_mode_id == const.DYNAMIC_REF:
  87 + coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :])
  88 + else:
  89 + coord = coord_raw[0, :]
  90 + coord[2] = -coord[2]
  91 +
  92 + coord_raw_samples[i] = coord_raw
  93 + coord_samples[i] = coord
  94 +
  95 + coord_raw_avg = np.median(list(coord_raw_samples.values()), axis=0)
  96 + coord_avg = np.median(list(coord_samples.values()), axis=0)
  97 +
  98 + return coord_avg, coord_raw_avg
  99 +
  100 + def SetTrackerFiducial(self, ref_mode_id, fiducial_index):
  101 + coord, coord_raw = self.GetTrackerCoordinates(
  102 + ref_mode_id=ref_mode_id,
  103 + n_samples=const.CALIBRATION_TRACKER_SAMPLES,
  104 + )
  105 +
  106 + # Update tracker fiducial with tracker coordinates
  107 + self.tracker_fiducials[fiducial_index, :] = coord[0:3]
  108 +
  109 + assert 0 <= fiducial_index <= 2, "Fiducial index out of range (0-2): {}".format(fiducial_index)
  110 +
  111 + self.tracker_fiducials_raw[2 * fiducial_index, :] = coord_raw[0, :]
  112 + self.tracker_fiducials_raw[2 * fiducial_index + 1, :] = coord_raw[1, :]
  113 +
  114 + print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3]))
  115 +
  116 + def ResetTrackerFiducials(self):
  117 + for m in range(3):
  118 + self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan]
  119 +
  120 + def GetTrackerFiducials(self):
  121 + return self.tracker_fiducials, self.tracker_fiducials_raw
  122 +
  123 + def GetTrackerInfo(self):
  124 + return self.trk_init, self.tracker_id
  125 +
  126 + def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre):
  127 + if self.tracker_connected:
  128 + selection_ctrl.SetSelection(self.tracker_id)
  129 + else:
  130 + selection_ctrl.SetSelection(0)
  131 +
  132 + # Update tracker location in the UI.
  133 + for m in range(3):
  134 + coord = self.tracker_fiducials[m, :]
  135 + for n in range(0, 3):
  136 + value = 0.0 if np.isnan(coord[n]) else float(coord[n])
  137 + numctrls_fiducial[m][n].SetValue(value)
  138 +
  139 + txtctrl_fre.SetValue('')
  140 + txtctrl_fre.SetBackgroundColour('WHITE')
  141 +
  142 + def get_trackers(self):
  143 + return const.TRACKERS