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

.gitignore
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 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 290 *.aux
  291 +*.lof
  292 +*.log
  293 +*.lot
  294 +*.fls
  295 +*.out
41 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 316 *.bbl
  317 +*.bcf
43 318 *.blg
44   -*.fls
  319 +*-blx.aux
  320 +*-blx.bib
  321 +*.run.xml
  322 +
  323 +## Build tool auxiliary files:
45 324 *.fdb_latexmk
  325 +*.synctex
  326 +*.synctex(busy)
46 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 761 OBJC = wx.NewId()
762 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 797 MTC_PROBE_NAME = "1Probe"
777 798 MTC_REF_NAME = "2Ref"
... ...
invesalius/data/bases.py
... ... @@ -188,6 +188,13 @@ def object_registration(fiducials, orients, coord_raw, m_change):
188 188 fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3]
189 189  
190 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 198 s0_raw = dco.coordinates_to_transformation_matrix(
192 199 position=coords[3, :3],
193 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 149 # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html
150 150 def gdcm_to_numpy(image, apply_intercep_scale=True):
151 151 map_gdcm_np = {
  152 + gdcm.PixelFormat.SINGLEBIT: np.uint8,
152 153 gdcm.PixelFormat.UINT8: np.uint8,
153 154 gdcm.PixelFormat.INT8: np.int8,
154 155 gdcm.PixelFormat.UINT12: np.uint16,
... ... @@ -177,6 +178,8 @@ def gdcm_to_numpy(image, apply_intercep_scale=True):
177 178 np_array = np.frombuffer(
178 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 183 np_array.shape = shape
181 184 np_array = np_array.squeeze()
182 185  
... ...
invesalius/gui/dialogs.py
... ... @@ -3304,8 +3304,9 @@ class MaskDensityDialog(wx.Dialog):
3304 3304  
3305 3305 class ObjectCalibrationDialog(wx.Dialog):
3306 3306  
3307   - def __init__(self, tracker):
  3307 + def __init__(self, tracker, pedal_connection):
3308 3308 self.tracker = tracker
  3309 + self.pedal_connection = pedal_connection
3309 3310  
3310 3311 self.trk_init, self.tracker_id = tracker.GetTrackerInfo()
3311 3312  
... ... @@ -3313,6 +3314,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3313 3314 self.obj_name = None
3314 3315 self.polydata = None
3315 3316 self.use_default_object = False
  3317 + self.object_fiducial_being_set = None
3316 3318  
3317 3319 self.obj_fiducials = np.full([5, 3], np.nan)
3318 3320 self.obj_orients = np.full([5, 3], np.nan)
... ... @@ -3323,6 +3325,11 @@ class ObjectCalibrationDialog(wx.Dialog):
3323 3325 self._init_gui()
3324 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 3333 def _init_gui(self):
3327 3334 self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize())
3328 3335 self.interactor.Enable(1)
... ... @@ -3373,15 +3380,17 @@ class ObjectCalibrationDialog(wx.Dialog):
3373 3380 choice_sensor])
3374 3381  
3375 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 3395 for m in range(0, 5):
3387 3396 for n in range(0, 3):
... ... @@ -3525,13 +3534,42 @@ class ObjectCalibrationDialog(wx.Dialog):
3525 3534 self.ren.AddActor(ball_actor)
3526 3535 return ball_actor, tactor
3527 3536  
3528   - def OnGetObjectFiducials(self, evt):
  3537 + def OnObjectFiducialButton(self, index, evt, ctrl):
3529 3538 if not self.tracker.IsTrackerInitialized():
3530 3539 ShowNavigationTrackerWarning(0, 'choose')
3531 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 3573 coord, coord_raw = self.tracker.GetTrackerCoordinates(
3536 3574 # XXX: Always use static reference mode when getting the coordinates. This is what the
3537 3575 # code did previously, as well. At some point, it should probably be thought through
... ... @@ -3549,22 +3587,22 @@ class ObjectCalibrationDialog(wx.Dialog):
3549 3587 # mode" principle above, but it's hard to come up with a simple change to increase the consistency
3550 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 3591 coord = coord_raw[self.obj_ref_id, :]
3554 3592 coord[2] = -coord[2]
3555 3593  
3556   - if btn_id == 3:
  3594 + if fiducial_index == 3:
3557 3595 coord = np.zeros([6,])
3558 3596  
3559 3597 # Update text controls with tracker coordinates
3560 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 3606 self.Refresh()
3569 3607 else:
3570 3608 ShowNavigationTrackerWarning(0, 'choose')
... ...
invesalius/gui/task_navigator.py
... ... @@ -18,12 +18,9 @@
18 18 #--------------------------------------------------------------------------
19 19  
20 20 from functools import partial
  21 +import itertools
21 22 import csv
22   -import os
23   -import queue
24   -import sys
25 23 import time
26   -import threading
27 24  
28 25 import nibabel as nb
29 26 import numpy as np
... ... @@ -35,10 +32,8 @@ except ImportError:
35 32 import wx
36 33  
37 34 try:
38   - import wx.lib.agw.hyperlink as hl
39 35 import wx.lib.agw.foldpanelbar as fpb
40 36 except ImportError:
41   - import wx.lib.hyperlink as hl
42 37 import wx.lib.foldpanelbar as fpb
43 38  
44 39 import wx.lib.colourselect as csel
... ... @@ -47,25 +42,22 @@ from invesalius.pubsub import pub as Publisher
47 42 from time import sleep
48 43  
49 44 import invesalius.constants as const
50   -import invesalius.data.bases as db
51 45  
52 46 if has_trekker:
53 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 49 import invesalius.data.imagedata_utils as imagedata_utils
58   -import invesalius.data.serial_port_connection as spc
59 50 import invesalius.data.slice_ as sl
60   -import invesalius.data.trackers as dt
61 51 import invesalius.data.tractography as dti
62   -import invesalius.data.transformations as tr
63 52 import invesalius.data.record_coords as rec
64 53 import invesalius.data.vtk_utils as vtk_utils
65 54 import invesalius.gui.dialogs as dlg
66 55 import invesalius.project as prj
67 56 from invesalius import utils
68 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 62 HAS_PEDAL_CONNECTION = True
71 63 try:
... ... @@ -161,9 +153,10 @@ class InnerFoldPanel(wx.Panel):
161 153 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
162 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 158 tracker = Tracker()
  159 + pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None
167 160  
168 161 # Fold panel style
169 162 style = fpb.CaptionBarStyle()
... ... @@ -173,7 +166,7 @@ class InnerFoldPanel(wx.Panel):
173 166  
174 167 # Fold 1 - Navigation panel
175 168 item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True)
176   - ntw = NeuronavigationPanel(item, tracker)
  169 + ntw = NeuronavigationPanel(item, tracker, pedal_connection)
177 170  
178 171 fold_panel.ApplyCaptionStyle(item, style)
179 172 fold_panel.AddFoldPanelWindow(item, ntw, spacing=0,
... ... @@ -182,7 +175,7 @@ class InnerFoldPanel(wx.Panel):
182 175  
183 176 # Fold 2 - Object registration panel
184 177 item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True)
185   - otw = ObjectRegistrationPanel(item, tracker)
  178 + otw = ObjectRegistrationPanel(item, tracker, pedal_connection)
186 179  
187 180 fold_panel.ApplyCaptionStyle(item, style)
188 181 fold_panel.AddFoldPanelWindow(item, otw, spacing=0,
... ... @@ -315,390 +308,8 @@ class InnerFoldPanel(wx.Panel):
315 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 311 class NeuronavigationPanel(wx.Panel):
701   - def __init__(self, parent, tracker):
  312 + def __init__(self, parent, tracker, pedal_connection):
702 313 wx.Panel.__init__(self, parent)
703 314 try:
704 315 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
... ... @@ -711,15 +322,16 @@ class NeuronavigationPanel(wx.Panel):
711 322 self.__bind_events()
712 323  
713 324 # Initialize global variables
714   - self.pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None
  325 + self.pedal_connection = pedal_connection
715 326 self.navigation = Navigation(
716   - pedal_connection=self.pedal_connection,
  327 + pedal_connection=pedal_connection,
717 328 )
718 329 self.icp = ICP()
719 330 self.tracker = tracker
720 331  
721 332 self.nav_status = False
722 333 self.tracker_fiducial_being_set = None
  334 + self.current_coord = 0, 0, 0
723 335  
724 336 # Initialize list of buttons and numctrls for wx objects
725 337 self.btns_set_fiducial = [None, None, None, None, None, None]
... ... @@ -776,7 +388,7 @@ class NeuronavigationPanel(wx.Panel):
776 388 txt_fre = wx.StaticText(self, -1, _('FRE:'))
777 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 392 txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:'))
781 393 else:
782 394 txt_pedal_pressed = None
... ... @@ -805,14 +417,14 @@ class NeuronavigationPanel(wx.Panel):
805 417 self.checkbox_icp = checkbox_icp
806 418  
807 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 421 tooltip = wx.ToolTip(_(u"Is the pedal pressed"))
810 422 checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' '))
811 423 checkbox_pedal_pressed.SetValue(False)
812 424 checkbox_pedal_pressed.Enable(False)
813 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 429 self.checkbox_pedal_pressed = checkbox_pedal_pressed
818 430 else:
... ... @@ -846,7 +458,7 @@ class NeuronavigationPanel(wx.Panel):
846 458 (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
847 459  
848 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 462 pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL),
851 463 (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
852 464  
... ... @@ -982,7 +594,7 @@ class NeuronavigationPanel(wx.Panel):
982 594  
983 595 def UpdateImageCoordinates(self, position):
984 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 599 for m in [0, 1, 2]:
988 600 if not self.btns_set_fiducial[m].GetValue():
... ... @@ -1039,7 +651,7 @@ class NeuronavigationPanel(wx.Panel):
1039 651 fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name']
1040 652  
1041 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 656 if self.btns_set_fiducial[n].GetValue():
1045 657 coord = self.numctrls_fiducial[n][0].GetValue(),\
... ... @@ -1053,13 +665,13 @@ class NeuronavigationPanel(wx.Panel):
1053 665 seed = 3 * [0.]
1054 666  
1055 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 669 else:
1058 670 for m in [0, 1, 2]:
1059 671 self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m]))
1060 672  
1061 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 676 def OnTrackerFiducials(self, n, evt, ctrl):
1065 677  
... ... @@ -1074,14 +686,16 @@ class NeuronavigationPanel(wx.Panel):
1074 686 def set_fiducial_callback():
1075 687 fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name']
1076 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 692 ctrl.SetValue(False)
1080 693 self.tracker_fiducial_being_set = None
1081 694  
1082 695 if ctrl.GetValue():
1083 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 699 else:
1086 700 set_fiducial_callback()
1087 701  
... ... @@ -1195,7 +809,7 @@ class NeuronavigationPanel(wx.Panel):
1195 809  
1196 810  
1197 811 class ObjectRegistrationPanel(wx.Panel):
1198   - def __init__(self, parent, tracker):
  812 + def __init__(self, parent, tracker, pedal_connection):
1199 813 wx.Panel.__init__(self, parent)
1200 814 try:
1201 815 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
... ... @@ -1206,6 +820,7 @@ class ObjectRegistrationPanel(wx.Panel):
1206 820 self.coil_list = const.COIL
1207 821  
1208 822 self.tracker = tracker
  823 + self.pedal_connection = pedal_connection
1209 824  
1210 825 self.nav_prop = None
1211 826 self.obj_fiducials = None
... ... @@ -1368,7 +983,7 @@ class ObjectRegistrationPanel(wx.Panel):
1368 983 def OnLinkCreate(self, event=None):
1369 984  
1370 985 if self.tracker.IsTrackerInitialized():
1371   - dialog = dlg.ObjectCalibrationDialog(self.tracker)
  986 + dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection)
1372 987 try:
1373 988 if dialog.ShowModal() == wx.ID_OK:
1374 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 1094 self.current_angle = 0, 0, 0
1480 1095 self.current_seed = 0, 0, 0
1481 1096 self.list_coord = []
1482   - self.marker_ind = 0
1483 1097 self.tgt_flag = self.tgt_index = None
1484 1098 self.nav_status = False
1485 1099  
... ... @@ -1525,7 +1139,7 @@ class MarkersPanel(wx.Panel):
1525 1139  
1526 1140 # Buttons to delete or remove markers
1527 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 1144 btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23))
1531 1145 btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers)
... ... @@ -1564,7 +1178,7 @@ class MarkersPanel(wx.Panel):
1564 1178 def __bind_events(self):
1565 1179 # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points')
1566 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 1182 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers')
1569 1183 Publisher.subscribe(self.CreateMarker, 'Create marker')
1570 1184 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
... ... @@ -1680,7 +1294,6 @@ class MarkersPanel(wx.Panel):
1680 1294  
1681 1295 if result == wx.ID_OK:
1682 1296 self.list_coord = []
1683   - self.marker_ind = 0
1684 1297 Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount())
1685 1298 self.lc.DeleteAllItems()
1686 1299 Publisher.sendMessage('Stop Blink Marker', index='DeleteAll')
... ... @@ -1691,46 +1304,47 @@ class MarkersPanel(wx.Panel):
1691 1304 if not hasattr(evt, 'data'):
1692 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 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 1316 for id_n in range(self.lc.GetItemCount()):
1701 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 1326 if index:
1715 1327 if self.tgt_flag and self.tgt_index == index[0]:
1716 1328 self.tgt_flag = self.tgt_index = None
1717 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 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 1340 for i in reversed(index):
1726 1341 del self.list_coord[i]
1727 1342 self.lc.DeleteItem(i)
1728 1343 for n in range(0, self.lc.GetItemCount()):
1729 1344 self.lc.SetItem(n, 0, str(n+1))
1730   - self.marker_ind -= 1
1731 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 1348 self.CreateMarker()
1735 1349  
1736 1350 def OnLoadMarkers(self, evt):
... ... @@ -1792,7 +1406,7 @@ class MarkersPanel(wx.Panel):
1792 1406 marker_id = '*'
1793 1407  
1794 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 1411 # if there are multiple TARGETS will set the last one
1798 1412 if target:
... ... @@ -1847,7 +1461,7 @@ class MarkersPanel(wx.Panel):
1847 1461 target_id = line[20]
1848 1462  
1849 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 1466 # if there are multiple TARGETS will set the last one
1853 1467 if target:
... ... @@ -1903,7 +1517,7 @@ class MarkersPanel(wx.Panel):
1903 1517 def OnSelectSize(self, evt, ctrl):
1904 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 1521 coord = coord or self.current_coord
1908 1522 colour = colour or self.marker_colour
1909 1523 size = size or self.marker_size
... ... @@ -1917,9 +1531,7 @@ class MarkersPanel(wx.Panel):
1917 1531 # TODO: Use matrix coordinates and not world coordinates as current method.
1918 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 1536 # List of lists with coordinates and properties of a marker
1925 1537 line = []
... ... @@ -1928,7 +1540,7 @@ class MarkersPanel(wx.Panel):
1928 1540 line.extend(orientation_world)
1929 1541 line.extend(colour)
1930 1542 line.append(size)
1931   - line.append(marker_id)
  1543 + line.append(label)
1932 1544 line.extend(seed)
1933 1545 line.append(target_id)
1934 1546  
... ... @@ -1944,21 +1556,24 @@ class MarkersPanel(wx.Panel):
1944 1556 self.lc.SetItem(num_items, 1, str(round(coord[0], 2)))
1945 1557 self.lc.SetItem(num_items, 2, str(round(coord[1], 2)))
1946 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 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 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 1574 return selection
1961 1575  
  1576 +
1962 1577 class DbsPanel(wx.Panel):
1963 1578 def __init__(self, parent):
1964 1579 wx.Panel.__init__(self, parent)
... ... @@ -2410,100 +2025,6 @@ class TractographyPanel(wx.Panel):
2410 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 2028 class InputAttributes(object):
2508 2029 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
2509 2030 def __init__(self, *initial_data, **kwargs):
... ...
invesalius/navigation/icp.py 0 → 100644
... ... @@ -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 @@
  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 @@
  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
... ...