Commit 7a197822cdfc2f1c59513769e53a5d09138462a4

Authored by Renan
2 parents 329c784e 490d87a8
Exists in master

Merge branch 'master' into multimodal_tracking

# Conflicts:
#	invesalius/constants.py
#	invesalius/data/coordinates.py
#	invesalius/gui/dialogs.py
#	invesalius/gui/task_navigator.py
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 +.idea/
  12 +
  13 +# CMake
  14 +cmake-build-*/
  15 +
  16 +# File-based project format
  17 +*.iws
  18 +
  19 +# IntelliJ
  20 +out/
  21 +
  22 +# mpeltonen/sbt-idea plugin
  23 +.idea_modules/
  24 +
  25 +# JIRA plugin
  26 +atlassian-ide-plugin.xml
  27 +
  28 +# Crashlytics plugin (for Android Studio and IntelliJ)
  29 +com_crashlytics_export_strings.xml
  30 +crashlytics.properties
  31 +crashlytics-build.properties
  32 +fabric.properties
  33 +
  34 +### Python ###
  35 +# Byte-compiled / optimized / DLL files
  36 +__pycache__/
  37 +*.py[cod]
  38 +*$py.class
  39 +
  40 +# C extensions
23 *.so 41 *.so
24 -tags  
25 -*.c  
26 42
27 -.idea  
28 -build  
29 -*.patch  
30 -*.tgz 43 +# Distribution / packaging
  44 +.Python
  45 +build/
  46 +develop-eggs/
  47 +dist/
  48 +downloads/
  49 +eggs/
  50 +.eggs/
  51 +lib/
  52 +lib64/
  53 +parts/
  54 +sdist/
  55 +var/
  56 +wheels/
  57 +share/python-wheels/
  58 +*.egg-info/
  59 +.installed.cfg
  60 +*.egg
  61 +MANIFEST
31 62
32 -*.pyd  
33 -*.cpp  
34 -*.diff 63 +# PyInstaller
  64 +# Usually these files are written by a python script from a template
  65 +# before PyInstaller builds the exe, so as to inject date/other infos into it.
  66 +*.manifest
  67 +*.spec
  68 +
  69 +# Installer logs
  70 +pip-log.txt
  71 +pip-delete-this-directory.txt
  72 +
  73 +# Unit test / coverage reports
  74 +htmlcov/
  75 +.tox/
  76 +.nox/
  77 +.coverage
  78 +.coverage.*
  79 +.cache
  80 +nosetests.xml
  81 +coverage.xml
  82 +*.cover
  83 +*.py,cover
  84 +.hypothesis/
  85 +.pytest_cache/
  86 +cover/
  87 +
  88 +# Translations
  89 +*.mo
  90 +*.pot
  91 +
  92 +# Django stuff:
  93 +*.log
  94 +local_settings.py
  95 +db.sqlite3
  96 +db.sqlite3-journal
  97 +
  98 +# Flask stuff:
  99 +instance/
  100 +.webassets-cache
  101 +
  102 +# Scrapy stuff:
  103 +.scrapy
  104 +
  105 +# Sphinx documentation
  106 +docs/_build/
  107 +
  108 +# PyBuilder
  109 +.pybuilder/
  110 +target/
  111 +
  112 +# Jupyter Notebook
  113 +.ipynb_checkpoints
  114 +
  115 +# IPython
  116 +profile_default/
  117 +ipython_config.py
  118 +
  119 +# pyenv
  120 +# For a library or package, you might want to ignore these files since the code is
  121 +# intended to run in multiple environments; otherwise, check them in:
  122 +# .python-version
  123 +
  124 +# pipenv
  125 +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
  126 +# However, in case of collaboration, if having platform-specific dependencies or dependencies
  127 +# having no cross-platform support, pipenv may install dependencies that don't work, or not
  128 +# install all needed dependencies.
  129 +#Pipfile.lock
  130 +
  131 +# PEP 582; used by e.g. github.com/David-OConnor/pyflow
  132 +__pypackages__/
  133 +
  134 +# Celery stuff
  135 +celerybeat-schedule
  136 +celerybeat.pid
  137 +
  138 +# SageMath parsed files
  139 +*.sage.py
35 140
36 -*.directory 141 +# Environments
  142 +.env
  143 +.venv
  144 +env/
  145 +venv/
  146 +ENV/
  147 +env.bak/
  148 +venv.bak/
  149 +myenv/
37 150
  151 +# Spyder project settings
  152 +.spyderproject
  153 +.spyproject
38 154
39 -# latex 155 +# Rope project settings
  156 +.ropeproject
  157 +
  158 +# mkdocs documentation
  159 +/site
  160 +
  161 +# mypy
  162 +.mypy_cache/
  163 +.dmypy.json
  164 +dmypy.json
  165 +
  166 +# Pyre type checker
  167 +.pyre/
  168 +
  169 +# pytype static type analyzer
  170 +.pytype/
  171 +
  172 +# Cython debug symbols
  173 +cython_debug/
  174 +
  175 +### Vim ###
  176 +# Swap
  177 +[._]*.s[a-v][a-z]
  178 +!*.svg # comment out if you don't need vector files
  179 +[._]*.sw[a-p]
  180 +[._]s[a-rt-v][a-z]
  181 +[._]ss[a-gi-z]
  182 +[._]sw[a-p]
  183 +
  184 +# Session
  185 +Session.vim
  186 +Sessionx.vim
  187 +
  188 +# Temporary
  189 +.netrwhist
  190 +*~
  191 +# Auto-generated tag files
  192 +tags
  193 +# Persistent undo
  194 +[._]*.un~
  195 +
  196 +### VisualStudioCode ###
  197 +.vscode/
  198 +*.code-workspace
  199 +
  200 +# Local History for Visual Studio Code
  201 +.history/
  202 +
  203 +### VisualStudioCode Patch ###
  204 +# Ignore all local history of files
  205 +.history
  206 +.ionide
  207 +
  208 +### LaTeX ###
  209 +## Core latex/pdflatex auxiliary files:
40 *.aux 210 *.aux
  211 +*.lof
  212 +*.log
  213 +*.lot
  214 +*.fls
  215 +*.out
41 *.toc 216 *.toc
  217 +*.fmt
  218 +*.fot
  219 +*.cb
  220 +*.cb2
  221 +.*.lb
  222 +
  223 +## Intermediate documents:
  224 +*.dvi
  225 +*.xdv
  226 +*-converted-to.*
  227 +# these rules might exclude image files for figures etc.
  228 +# *.ps
  229 +# *.eps
  230 +# *.pdf
  231 +
  232 +## Generated if empty string is given at "Please type another file name for output:"
  233 +.pdf
  234 +
  235 +## Bibliography auxiliary files (bibtex/biblatex/biber):
42 *.bbl 236 *.bbl
  237 +*.bcf
43 *.blg 238 *.blg
44 -*.fls 239 +*-blx.aux
  240 +*-blx.bib
  241 +*.run.xml
  242 +
  243 +## Build tool auxiliary files:
45 *.fdb_latexmk 244 *.fdb_latexmk
  245 +*.synctex
  246 +*.synctex(busy)
46 *.synctex.gz 247 *.synctex.gz
47 -*.out  
48 -*.log 248 +*.synctex.gz(busy)
  249 +*.pdfsync
  250 +
  251 +# End of https://www.toptal.com/developers/gitignore/api/vim,python,intellij,visualstudiocode,direnv
  252 +
  253 +### Cython generated files ###
  254 +*.c
  255 +*.cpp
  256 +
  257 +### Patches and diffs ###
  258 +*.patch
  259 +*.diff
@@ -209,9 +209,9 @@ class Inv3SplashScreen(SplashScreen): @@ -209,9 +209,9 @@ class Inv3SplashScreen(SplashScreen):
209 209
210 else: 210 else:
211 211
212 - path = os.path.join(".","icons", icon_file) 212 + path = os.path.join(inv_paths.ICON_DIR, icon_file)
213 if not os.path.exists(path): 213 if not os.path.exists(path):
214 - path = os.path.join(".", "icons", "splash_en.png") 214 + path = os.path.join(inv_paths.ICON_DIR, "splash_en.png")
215 215
216 bmp = wx.Image(path).ConvertToBitmap() 216 bmp = wx.Image(path).ConvertToBitmap()
217 217
environment.yml
@@ -3,25 +3,23 @@ channels: @@ -3,25 +3,23 @@ channels:
3 - bioconda 3 - bioconda
4 dependencies: 4 dependencies:
5 - python=3.7 5 - python=3.7
6 - - cython==0.29.22  
7 - - pillow==8.1.1  
8 - - wxpython==4.1.0 6 + - cython==0.29.24
  7 + - pillow==8.3.2
9 - pypubsub==4.0.3 8 - pypubsub==4.0.3
10 - configparser==5.0.1 9 - configparser==5.0.1
11 - h5py==2.10.0 10 - h5py==2.10.0
12 - imageio==2.9.0 11 - imageio==2.9.0
13 - nibabel==3.2.1 12 - nibabel==3.2.1
14 - - numpy==1.20.1  
15 - - pooch==1.4.0 13 + - numpy==1.21.2
16 - psutil==5.8.0 14 - psutil==5.8.0
17 - pyserial==3.5 15 - pyserial==3.5
18 - - scikit-image==0.18.1  
19 - - scipy==1.6.1  
20 - - vtk==9.0.1  
21 - - wxpython==4.1.0 16 + - scikit-image==0.18.3
  17 + - scipy==1.7.1
  18 + - vtk==9.0.3
  19 + - wxpython==4.1.1
22 - pip 20 - pip
23 - pip: 21 - pip:
24 - - python-gdcm==3.0.8.1 22 + - python-gdcm==3.0.9.1
25 - plaidml-keras==0.7.0 23 - plaidml-keras==0.7.0
26 - theano==1.0.5 24 - theano==1.0.5
27 - pyacvd==0.2.3 25 - pyacvd==0.2.3
invesalius/constants.py
@@ -659,6 +659,8 @@ BOOLEAN_XOR = 4 @@ -659,6 +659,8 @@ BOOLEAN_XOR = 4
659 659
660 MARKER_COLOUR = (1.0, 1.0, 0.) 660 MARKER_COLOUR = (1.0, 1.0, 0.)
661 MARKER_SIZE = 2 661 MARKER_SIZE = 2
  662 +
  663 +CALIBRATION_TRACKER_SAMPLES = 10
662 FIDUCIAL_REGISTRATION_ERROR_THRESHOLD = 3.0 664 FIDUCIAL_REGISTRATION_ERROR_THRESHOLD = 3.0
663 665
664 SELECT = 0 666 SELECT = 0
@@ -760,21 +762,42 @@ OBJA = wx.NewId() @@ -760,21 +762,42 @@ OBJA = wx.NewId()
760 OBJC = wx.NewId() 762 OBJC = wx.NewId()
761 OBJF = wx.NewId() 763 OBJF = wx.NewId()
762 764
763 -BTNS_OBJ = {OBJL: {0: _('Left')},  
764 - OBJR: {1: _('Right')},  
765 - OBJA: {2: _('Anterior')},  
766 - OBJC: {3: _('Center')},  
767 - OBJF: {4: _('Fixed')}}  
768 -  
769 -TIPS_OBJ = [_("Select left object fiducial"),  
770 - _("Select right object fiducial"),  
771 - _("Select anterior object fiducial"),  
772 - _("Select object center"),  
773 - _("Attach sensor to object")] 765 +OBJECT_FIDUCIALS = [
  766 + {
  767 + 'fiducial_index': 0,
  768 + 'button_id': OBJL,
  769 + 'label': _('Left'),
  770 + 'tip': _("Select left object fiducial"),
  771 + },
  772 + {
  773 + 'fiducial_index': 1,
  774 + 'button_id': OBJR,
  775 + 'label': _('Right'),
  776 + 'tip': _("Select right object fiducial"),
  777 + },
  778 + {
  779 + 'fiducial_index': 2,
  780 + 'button_id': OBJA,
  781 + 'label': _('Anterior'),
  782 + 'tip': _("Select anterior object fiducial"),
  783 + },
  784 + {
  785 + 'fiducial_index': 3,
  786 + 'button_id': OBJC,
  787 + 'label': _('Center'),
  788 + 'tip': _("Select object center"),
  789 + },
  790 + {
  791 + 'fiducial_index': 4,
  792 + 'button_id': OBJF,
  793 + 'label': _('Fixed'),
  794 + 'tip': _("Attach sensor to object"),
  795 + },
  796 +]
774 797
775 MTC_PROBE_NAME = "1Probe" 798 MTC_PROBE_NAME = "1Probe"
776 MTC_REF_NAME = "2Ref" 799 MTC_REF_NAME = "2Ref"
777 -MTC_OBJ_NAME = "3bigcoil" 800 +MTC_OBJ_NAME = "3Coil"
778 801
779 # Object tracking 802 # Object tracking
780 ARROW_SCALE = 6 803 ARROW_SCALE = 6
@@ -805,8 +828,9 @@ TREKKER_CONFIG = {'seed_max': 1, 'step_size': 0.1, 'min_fod': 0.1, 'probe_qualit @@ -805,8 +828,9 @@ TREKKER_CONFIG = {'seed_max': 1, 'step_size': 0.1, 'min_fod': 0.1, 'probe_qualit
805 'write_interval': 50, 'numb_threads': '', 'max_lenth': 200, 828 'write_interval': 50, 'numb_threads': '', 'max_lenth': 200,
806 'min_lenth': 20, 'max_sampling_step': 100} 829 'min_lenth': 20, 'max_sampling_step': 100}
807 830
808 -WILDCARD_MARKER_FILES = _("Marker scanner coord files (*.mkss)|*.mkss") + "|" +\  
809 - _("Marker files (*.mks)|*.mks") 831 +MARKER_FILE_MAGICK_STRING = "INVESALIUS3_MARKER_FILE_"
  832 +CURRENT_MARKER_FILE_VERSION = 0
  833 +WILDCARD_MARKER_FILES = _("Marker scanner coord files (*.mkss)|*.mkss")
810 834
811 ROBOT_ElFIN_IP = ['Select robot IP:', '143.107.220.251', '169.254.153.251', '127.0.0.1'] 835 ROBOT_ElFIN_IP = ['Select robot IP:', '143.107.220.251', '169.254.153.251', '127.0.0.1']
812 ROBOT_ElFIN_PORT = 10003 836 ROBOT_ElFIN_PORT = 10003
invesalius/control.py
@@ -915,6 +915,16 @@ class Controller(): @@ -915,6 +915,16 @@ class Controller():
915 matrix, matrix_filename = self.OpenOtherFiles(group) 915 matrix, matrix_filename = self.OpenOtherFiles(group)
916 self.CreateOtherProject(name, matrix, matrix_filename) 916 self.CreateOtherProject(name, matrix, matrix_filename)
917 self.LoadProject() 917 self.LoadProject()
  918 + if group.affine.any():
  919 + # TODO: replace the inverse of the affine by the actual affine in the whole code
  920 + # remove scaling factor for non-unitary voxel dimensions
  921 + # self.affine = image_utils.world2invspace(affine=group.affine)
  922 + scale, shear, angs, trans, persp = tr.decompose_matrix(group.affine)
  923 + self.affine = np.linalg.inv(tr.compose_matrix(scale=None, shear=shear,
  924 + angles=angs, translate=trans, perspective=persp))
  925 + # print("repos_img: {}".format(repos_img))
  926 + self.Slice.affine = self.affine
  927 + Publisher.sendMessage('Update affine matrix', affine=self.affine)
918 Publisher.sendMessage("Enable state project", state=True) 928 Publisher.sendMessage("Enable state project", state=True)
919 else: 929 else:
920 dialog.ImportInvalidFiles(ftype="Others") 930 dialog.ImportInvalidFiles(ftype="Others")
@@ -1043,17 +1053,6 @@ class Controller(): @@ -1043,17 +1053,6 @@ class Controller():
1043 self.Slice.window_level = wl 1053 self.Slice.window_level = wl
1044 self.Slice.window_width = ww 1054 self.Slice.window_width = ww
1045 1055
1046 - if group.affine.any():  
1047 - # TODO: replace the inverse of the affine by the actual affine in the whole code  
1048 - # remove scaling factor for non-unitary voxel dimensions  
1049 - # self.affine = image_utils.world2invspace(affine=group.affine)  
1050 - scale, shear, angs, trans, persp = tr.decompose_matrix(group.affine)  
1051 - self.affine = np.linalg.inv(tr.compose_matrix(scale=None, shear=shear,  
1052 - angles=angs, translate=trans, perspective=persp))  
1053 - # print("repos_img: {}".format(repos_img))  
1054 - self.Slice.affine = self.affine  
1055 - Publisher.sendMessage('Update affine matrix', affine=self.affine)  
1056 -  
1057 scalar_range = int(scalar_range[0]), int(scalar_range[1]) 1056 scalar_range = int(scalar_range[0]), int(scalar_range[1])
1058 Publisher.sendMessage('Update threshold limits list', 1057 Publisher.sendMessage('Update threshold limits list',
1059 threshold_range=scalar_range) 1058 threshold_range=scalar_range)
invesalius/data/bases.py
@@ -188,10 +188,18 @@ def object_registration(fiducials, orients, coord_raw, m_change): @@ -188,10 +188,18 @@ 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 - t_s0_raw = tr.translation_matrix(coords[3, :3])  
192 - r_s0_raw = tr.euler_matrix(np.radians(coords[3, 3]), np.radians(coords[3, 4]),  
193 - np.radians(coords[3, 5]), 'rzyx')  
194 - s0_raw = tr.concatenate_matrices(t_s0_raw, r_s0_raw) 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 +
  198 + s0_raw = dco.coordinates_to_transformation_matrix(
  199 + position=coords[3, :3],
  200 + orientation=coords[3, 3:],
  201 + axes='rzyx',
  202 + )
195 203
196 # compute change of basis for object fiducials in source frame 204 # compute change of basis for object fiducials in source frame
197 base_obj_raw, q_obj_raw = base_creation(fids_raw[:3, :3]) 205 base_obj_raw, q_obj_raw = base_creation(fids_raw[:3, :3])
@@ -210,10 +218,11 @@ def object_registration(fiducials, orients, coord_raw, m_change): @@ -210,10 +218,11 @@ def object_registration(fiducials, orients, coord_raw, m_change):
210 fids_dyn[ic, 2] = -fids_dyn[ic, 2] 218 fids_dyn[ic, 2] = -fids_dyn[ic, 2]
211 219
212 # compute object fiducials in vtk head frame 220 # compute object fiducials in vtk head frame
213 - a, b, g = np.radians(fids_dyn[ic, 3:])  
214 - T_p = tr.translation_matrix(fids_dyn[ic, :3])  
215 - R_p = tr.euler_matrix(a, b, g, 'rzyx')  
216 - M_p = tr.concatenate_matrices(T_p, R_p) 221 + M_p = dco.coordinates_to_transformation_matrix(
  222 + position=fids_dyn[ic, :3],
  223 + orientation=fids_dyn[ic, 3:],
  224 + axes='rzyx',
  225 + )
217 M_img = m_change @ M_p 226 M_img = m_change @ M_p
218 227
219 angles_img = np.degrees(np.asarray(tr.euler_from_matrix(M_img, 'rzyx'))) 228 angles_img = np.degrees(np.asarray(tr.euler_from_matrix(M_img, 'rzyx')))
@@ -228,10 +237,11 @@ def object_registration(fiducials, orients, coord_raw, m_change): @@ -228,10 +237,11 @@ def object_registration(fiducials, orients, coord_raw, m_change):
228 r_obj_img[:3, :3] = base_obj_img[:3, :3] 237 r_obj_img[:3, :3] = base_obj_img[:3, :3]
229 238
230 # compute initial alignment of probe fixed in the object in reference (or static) frame 239 # compute initial alignment of probe fixed in the object in reference (or static) frame
231 - s0_trans_dyn = tr.translation_matrix(fids_dyn[3, :3])  
232 - s0_rot_dyn = tr.euler_matrix(np.radians(fids_dyn[3, 3]), np.radians(fids_dyn[3, 4]),  
233 - np.radians(fids_dyn[3, 5]), 'rzyx')  
234 - s0_dyn = tr.concatenate_matrices(s0_trans_dyn, s0_rot_dyn) 240 + s0_dyn = dco.coordinates_to_transformation_matrix(
  241 + position=fids_dyn[3, :3],
  242 + orientation=fids_dyn[3, 3:],
  243 + axes='rzyx',
  244 + )
235 245
236 return t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img 246 return t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img
237 247
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/data/coordinates.py
@@ -39,9 +39,9 @@ class TrackerCoordinates(): @@ -39,9 +39,9 @@ class TrackerCoordinates():
39 def SetCoordinates(self, coord, markers_flag): 39 def SetCoordinates(self, coord, markers_flag):
40 self.coord = coord 40 self.coord = coord
41 self.markers_flag = markers_flag 41 self.markers_flag = markers_flag
  42 + wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)
42 43
43 def GetCoordinates(self): 44 def GetCoordinates(self):
44 - wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)  
45 return self.coord, self.markers_flag 45 return self.coord, self.markers_flag
46 46
47 47
@@ -179,7 +179,7 @@ def PolarisCoord(trck_init, trck_id, ref_mode): @@ -179,7 +179,7 @@ def PolarisCoord(trck_init, trck_id, ref_mode):
179 179
180 coord = np.vstack([coord1, coord2, coord3]) 180 coord = np.vstack([coord1, coord2, coord3])
181 181
182 - return coord, [trck.probeID, trck.refID, trck.coilID] 182 + return coord, [trck.probeID, trck.refID, trck.objID]
183 183
184 def ElfinCoord(trck_init): 184 def ElfinCoord(trck_init):
185 if len(trck_init) > 2: 185 if len(trck_init) > 2:
@@ -384,11 +384,45 @@ def DebugCoordRandom(trk_init, trck_id, ref_mode): @@ -384,11 +384,45 @@ def DebugCoordRandom(trk_init, trck_id, ref_mode):
384 # coord4 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200), 384 # coord4 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
385 # uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)]) 385 # uniform(-180.0, 180.0), uniform(-180.0, 180.0), uniform(-180.0, 180.0)])
386 386
387 - #Publisher.sendMessage('Sensors ID', probe_id=int(uniform(0, 5)), ref_id=int(uniform(0, 5)), obj_id=int(uniform(0, 5)))  
388 -  
389 return np.vstack([coord1, coord2, coord3, coord4]), [int(uniform(0, 5)), int(uniform(0, 5)), int(uniform(0, 5))] 387 return np.vstack([coord1, coord2, coord3, coord4]), [int(uniform(0, 5)), int(uniform(0, 5)), int(uniform(0, 5))]
390 388
391 389
  390 +def coordinates_to_transformation_matrix(position, orientation, axes='sxyz'):
  391 + """
  392 + Transform vectors consisting of position and orientation (in Euler angles) in 3d-space into a 4x4
  393 + transformation matrix that combines the rotation and translation.
  394 + :param position: A vector of three coordinates.
  395 + :param orientation: A vector of three Euler angles in degrees.
  396 + :param axes: The order in which the rotations are done for the axes. See transformations.py for details. Defaults to 'sxyz'.
  397 + :return: The transformation matrix (4x4).
  398 + """
  399 + a, b, g = np.radians(orientation)
  400 +
  401 + r_ref = tr.euler_matrix(a, b, g, axes=axes)
  402 + t_ref = tr.translation_matrix(position)
  403 +
  404 + m_img = tr.concatenate_matrices(t_ref, r_ref)
  405 +
  406 + return m_img
  407 +
  408 +
  409 +def transformation_matrix_to_coordinates(matrix, axes='sxyz'):
  410 + """
  411 + Given a matrix that combines the rotation and translation, return the position and the orientation
  412 + determined by the matrix. The orientation is given as three Euler angles.
  413 + The inverse of coordinates_of_transformation_matrix when the parameter 'axes' matches.
  414 + :param matrix: A 4x4 transformation matrix.
  415 + :param axes: The order in which the rotations are done for the axes. See transformations.py for details. Defaults to 'sxyz'.
  416 + :return: The position (a vector of length 3) and Euler angles for the orientation in degrees (a vector of length 3).
  417 + """
  418 + angles = tr.euler_from_matrix(matrix, axes=axes)
  419 + angles_as_deg = np.degrees(angles)
  420 +
  421 + translation = tr.translation_from_matrix(matrix)
  422 +
  423 + return translation, angles_as_deg
  424 +
  425 +
392 def dynamic_reference(probe, reference): 426 def dynamic_reference(probe, reference):
393 """ 427 """
394 Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama 428 Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama
@@ -435,11 +469,11 @@ def dynamic_reference_m(probe, reference): @@ -435,11 +469,11 @@ def dynamic_reference_m(probe, reference):
435 :param reference: sensor two defined as reference 469 :param reference: sensor two defined as reference
436 :return: rotated and translated coordinates 470 :return: rotated and translated coordinates
437 """ 471 """
438 - a, b, g = np.radians(reference[3:6])  
439 -  
440 - trans = tr.translation_matrix(reference[:3])  
441 - rot = tr.euler_matrix(a, b, g, 'rzyx')  
442 - affine = tr.concatenate_matrices(trans, rot) 472 + affine = coordinates_to_transformation_matrix(
  473 + position=reference[:3],
  474 + orientation=reference[3:],
  475 + axes='rzyx',
  476 + )
443 probe_4 = np.vstack((probe[:3].reshape([3, 1]), 1.)) 477 probe_4 = np.vstack((probe[:3].reshape([3, 1]), 1.))
444 coord_rot = np.linalg.inv(affine) @ probe_4 478 coord_rot = np.linalg.inv(affine) @ probe_4
445 # minus sign to the z coordinate 479 # minus sign to the z coordinate
@@ -479,15 +513,16 @@ def dynamic_reference_m2(probe, reference): @@ -479,15 +513,16 @@ def dynamic_reference_m2(probe, reference):
479 :return: rotated and translated coordinates 513 :return: rotated and translated coordinates
480 """ 514 """
481 515
482 - a, b, g = np.radians(reference[3:6])  
483 - a_p, b_p, g_p = np.radians(probe[3:6])  
484 -  
485 - T = tr.translation_matrix(reference[:3])  
486 - T_p = tr.translation_matrix(probe[:3])  
487 - R = tr.euler_matrix(a, b, g, 'rzyx')  
488 - R_p = tr.euler_matrix(a_p, b_p, g_p, 'rzyx')  
489 - M = tr.concatenate_matrices(T, R)  
490 - M_p = tr.concatenate_matrices(T_p, R_p) 516 + M = coordinates_to_transformation_matrix(
  517 + position=reference[:3],
  518 + orientation=reference[3:],
  519 + axes='rzyx',
  520 + )
  521 + M_p = coordinates_to_transformation_matrix(
  522 + position=probe[:3],
  523 + orientation=probe[3:],
  524 + axes='rzyx',
  525 + )
491 526
492 M_dyn = np.linalg.inv(M) @ M_p 527 M_dyn = np.linalg.inv(M) @ M_p
493 528
invesalius/data/coregistration.py
@@ -69,12 +69,11 @@ def object_to_reference(coord_raw, m_probe): @@ -69,12 +69,11 @@ def object_to_reference(coord_raw, m_probe):
69 :return: 4 x 4 numpy double array 69 :return: 4 x 4 numpy double array
70 :rtype: numpy.ndarray 70 :rtype: numpy.ndarray
71 """ 71 """
72 -  
73 - a, b, g = np.radians(coord_raw[1, 3:])  
74 - r_ref = tr.euler_matrix(a, b, g, 'rzyx')  
75 - t_ref = tr.translation_matrix(coord_raw[1, :3])  
76 - m_ref = tr.concatenate_matrices(t_ref, r_ref)  
77 - 72 + m_ref = dco.coordinates_to_transformation_matrix(
  73 + position=coord_raw[1, :3],
  74 + orientation=coord_raw[1, 3:],
  75 + axes='rzyx',
  76 + )
78 m_dyn = np.linalg.inv(m_ref) @ m_probe 77 m_dyn = np.linalg.inv(m_ref) @ m_probe
79 return m_dyn 78 return m_dyn
80 79
@@ -108,20 +107,25 @@ def corregistrate_object_dynamic(inp, coord_raw, ref_mode_id, icp): @@ -108,20 +107,25 @@ def corregistrate_object_dynamic(inp, coord_raw, ref_mode_id, icp):
108 107
109 # transform raw marker coordinate to object center 108 # transform raw marker coordinate to object center
110 m_probe = object_marker_to_center(coord_raw, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw) 109 m_probe = object_marker_to_center(coord_raw, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw)
  110 +
111 # transform object center to reference marker if specified as dynamic reference 111 # transform object center to reference marker if specified as dynamic reference
112 if ref_mode_id: 112 if ref_mode_id:
113 m_probe_ref = object_to_reference(coord_raw, m_probe) 113 m_probe_ref = object_to_reference(coord_raw, m_probe)
114 else: 114 else:
115 m_probe_ref = m_probe 115 m_probe_ref = m_probe
  116 +
116 # invert y coordinate 117 # invert y coordinate
117 m_probe_ref[2, -1] = -m_probe_ref[2, -1] 118 m_probe_ref[2, -1] = -m_probe_ref[2, -1]
  119 +
118 # corregistrate from tracker to image space 120 # corregistrate from tracker to image space
119 m_img = tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn) 121 m_img = tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn)
120 if icp[0]: 122 if icp[0]:
121 m_img = bases.transform_icp(m_img, icp[1]) 123 m_img = bases.transform_icp(m_img, icp[1])
  124 +
122 # compute rotation angles 125 # compute rotation angles
123 - _, _, angles, _, _ = tr.decompose_matrix(m_img)  
124 - # create output coordiante list 126 + angles = tr.euler_from_matrix(m_img, axes='sxyz')
  127 +
  128 + # create output coordinate list
125 coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \ 129 coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
126 np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2]) 130 np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2])
127 131
@@ -132,10 +136,11 @@ def UpdateICP(self, m_icp, flag): @@ -132,10 +136,11 @@ def UpdateICP(self, m_icp, flag):
132 self.icp = flag 136 self.icp = flag
133 137
134 def compute_marker_transformation(coord_raw, obj_ref_mode): 138 def compute_marker_transformation(coord_raw, obj_ref_mode):
135 - psi, theta, phi = np.radians(coord_raw[obj_ref_mode, 3:])  
136 - r_probe = tr.euler_matrix(psi, theta, phi, 'rzyx')  
137 - t_probe = tr.translation_matrix(coord_raw[obj_ref_mode, :3])  
138 - m_probe = tr.concatenate_matrices(t_probe, r_probe) 139 + m_probe = dco.coordinates_to_transformation_matrix(
  140 + position=coord_raw[obj_ref_mode, :3],
  141 + orientation=coord_raw[obj_ref_mode, 3:],
  142 + axes='rzyx',
  143 + )
139 return m_probe 144 return m_probe
140 145
141 146
@@ -145,6 +150,7 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp): @@ -145,6 +150,7 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp):
145 150
146 # transform raw marker coordinate to object center 151 # transform raw marker coordinate to object center
147 m_probe = compute_marker_transformation(coord_raw, obj_ref_mode) 152 m_probe = compute_marker_transformation(coord_raw, obj_ref_mode)
  153 +
148 # transform object center to reference marker if specified as dynamic reference 154 # transform object center to reference marker if specified as dynamic reference
149 if ref_mode_id: 155 if ref_mode_id:
150 m_ref = compute_marker_transformation(coord_raw, 1) 156 m_ref = compute_marker_transformation(coord_raw, 1)
@@ -154,6 +160,7 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp): @@ -154,6 +160,7 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp):
154 160
155 # invert y coordinate 161 # invert y coordinate
156 m_probe_ref[2, -1] = -m_probe_ref[2, -1] 162 m_probe_ref[2, -1] = -m_probe_ref[2, -1]
  163 +
157 # corregistrate from tracker to image space 164 # corregistrate from tracker to image space
158 m_img = m_change @ m_probe_ref 165 m_img = m_change @ m_probe_ref
159 166
@@ -161,8 +168,9 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp): @@ -161,8 +168,9 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp):
161 m_img = bases.transform_icp(m_img, icp[1]) 168 m_img = bases.transform_icp(m_img, icp[1])
162 169
163 # compute rotation angles 170 # compute rotation angles
164 - _, _, angles, _, _ = tr.decompose_matrix(m_img)  
165 - # create output coordiante list 171 + angles = tr.euler_from_matrix(m_img, axes='sxyz')
  172 +
  173 + # create output coordinate list
166 coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\ 174 coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\
167 np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2]) 175 np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2])
168 176
@@ -204,7 +212,7 @@ class CoordinateCorregistrate(threading.Thread): @@ -204,7 +212,7 @@ class CoordinateCorregistrate(threading.Thread):
204 coreg_data = self.coreg_data 212 coreg_data = self.coreg_data
205 view_obj = 1 213 view_obj = 1
206 214
207 - trck_init, trck_id, trck_mode = self.tracker.GetTrackerInfo() 215 + trck_init, trck_id = self.tracker.GetTrackerInfo()
208 216
209 # print('CoordCoreg: event {}'.format(self.event.is_set())) 217 # print('CoordCoreg: event {}'.format(self.event.is_set()))
210 while not self.event.is_set(): 218 while not self.event.is_set():
@@ -284,7 +292,7 @@ class CoordinateCorregistrateNoObject(threading.Thread): @@ -284,7 +292,7 @@ class CoordinateCorregistrateNoObject(threading.Thread):
284 coreg_data = self.coreg_data 292 coreg_data = self.coreg_data
285 view_obj = 0 293 view_obj = 0
286 294
287 - trck_init, trck_id, trck_mode = self.tracker.GetTrackerInfo() 295 + trck_init, trck_id = self.tracker.GetTrackerInfo()
288 # print('CoordCoreg: event {}'.format(self.event.is_set())) 296 # print('CoordCoreg: event {}'.format(self.event.is_set()))
289 while not self.event.is_set(): 297 while not self.event.is_set():
290 try: 298 try:
invesalius/data/imagedata_utils.py
@@ -34,6 +34,7 @@ from vtk.util import numpy_support @@ -34,6 +34,7 @@ from vtk.util import numpy_support
34 34
35 import invesalius.constants as const 35 import invesalius.constants as const
36 import invesalius.data.converters as converters 36 import invesalius.data.converters as converters
  37 +import invesalius.data.coordinates as dco
37 import invesalius.data.slice_ as sl 38 import invesalius.data.slice_ as sl
38 import invesalius.data.transformations as tr 39 import invesalius.data.transformations as tr
39 import invesalius.reader.bitmap_reader as bitmap_reader 40 import invesalius.reader.bitmap_reader as bitmap_reader
@@ -555,18 +556,23 @@ def image_normalize(image, min_=0.0, max_=1.0, output_dtype=np.int16): @@ -555,18 +556,23 @@ def image_normalize(image, min_=0.0, max_=1.0, output_dtype=np.int16):
555 return output 556 return output
556 557
557 558
  559 +# TODO: Add a description of different coordinate systems, namely:
  560 +# - the world coordinate system,
  561 +# - the voxel coordinate system.
  562 +# - InVesalius's internal coordinate system,
  563 +#
558 def convert_world_to_voxel(xyz, affine): 564 def convert_world_to_voxel(xyz, affine):
559 """ 565 """
560 Convert a coordinate from the world space ((x, y, z); scanner space; millimeters) to the 566 Convert a coordinate from the world space ((x, y, z); scanner space; millimeters) to the
561 voxel space ((i, j, k)). This is achieved by multiplying a coordinate by the inverse 567 voxel space ((i, j, k)). This is achieved by multiplying a coordinate by the inverse
562 of the affine transformation. 568 of the affine transformation.
  569 +
563 More information: https://nipy.org/nibabel/coordinate_systems.html 570 More information: https://nipy.org/nibabel/coordinate_systems.html
  571 +
564 :param xyz: a list or array of 3 coordinates (x, y, z) in the world coordinates 572 :param xyz: a list or array of 3 coordinates (x, y, z) in the world coordinates
565 :param affine: a 4x4 array containing the image affine transformation in homogeneous coordinates 573 :param affine: a 4x4 array containing the image affine transformation in homogeneous coordinates
566 :return: a 1x3 array with the point coordinates in image space (i, j, k) 574 :return: a 1x3 array with the point coordinates in image space (i, j, k)
567 """ 575 """
568 -  
569 - # print("xyz: ", xyz, "\naffine", affine)  
570 # convert xyz coordinate to 1x4 homogeneous coordinates array 576 # convert xyz coordinate to 1x4 homogeneous coordinates array
571 xyz_homo = np.hstack((xyz, 1.0)).reshape([4, 1]) 577 xyz_homo = np.hstack((xyz, 1.0)).reshape([4, 1])
572 ijk_homo = np.linalg.inv(affine) @ xyz_homo 578 ijk_homo = np.linalg.inv(affine) @ xyz_homo
@@ -575,6 +581,65 @@ def convert_world_to_voxel(xyz, affine): @@ -575,6 +581,65 @@ def convert_world_to_voxel(xyz, affine):
575 return ijk 581 return ijk
576 582
577 583
  584 +def convert_invesalius_to_voxel(position):
  585 + """
  586 + Convert position from InVesalius space to the voxel space.
  587 +
  588 + The two spaces are otherwise identical, but InVesalius space has a reverted y-axis
  589 + (increasing y-coordinate moves posterior in InVesalius space, but anterior in the voxel space).
  590 +
  591 + For instance, if the size of the voxel image is 256 x 256 x 160, the y-coordinate 0 in
  592 + InVesalius space corresponds to the y-coordinate 255 in the voxel space.
  593 +
  594 + :param position: a vector of 3 coordinates (x, y, z) in InVesalius space.
  595 + :return: a vector of 3 coordinates in the voxel space
  596 + """
  597 + slice = sl.Slice()
  598 + return np.array((position[0], slice.matrix.shape[1] - position[1] - 1, position[2]))
  599 +
  600 +
  601 +def convert_invesalius_to_world(position, orientation):
  602 + """
  603 + Convert position and orientation from InVesalius space to the world space.
  604 +
  605 + The axis definition for the Euler angles returned is 'sxyz', see transformations.py for more
  606 + information.
  607 +
  608 + Uses 'affine' matrix defined in the project created or opened by the user. If it is
  609 + undefined, return Nones as the coordinates for both position and orientation.
  610 +
  611 + More information: https://nipy.org/nibabel/coordinate_systems.html
  612 +
  613 + :param position: a vector of 3 coordinates in InVesalius space.
  614 + :param orientation: a vector of 3 Euler angles in InVesalius space.
  615 + :return: a pair consisting of 3 coordinates and 3 Euler angles in the world space, or Nones if
  616 + 'affine' matrix is not defined in the project.
  617 + """
  618 + slice = sl.Slice()
  619 +
  620 + if slice.affine is None:
  621 + position_world = (None, None, None)
  622 + orientation_world = (None, None, None)
  623 +
  624 + return position_world, orientation_world
  625 +
  626 + position_voxel = convert_invesalius_to_voxel(position)
  627 +
  628 + M_invesalius = dco.coordinates_to_transformation_matrix(
  629 + position=position_voxel,
  630 + orientation=orientation,
  631 + axes='sxyz',
  632 + )
  633 + M_world = np.linalg.inv(slice.affine) @ M_invesalius
  634 +
  635 + position_world, orientation_world = dco.transformation_matrix_to_coordinates(
  636 + M_world,
  637 + axes='sxyz',
  638 + )
  639 +
  640 + return position_world, orientation_world
  641 +
  642 +
578 def create_grid(xy_range, z_range, z_offset, spacing): 643 def create_grid(xy_range, z_range, z_offset, spacing):
579 x = np.arange(xy_range[0], xy_range[1] + 1, spacing) 644 x = np.arange(xy_range[0], xy_range[1] + 1, spacing)
580 y = np.arange(xy_range[0], xy_range[1] + 1, spacing) 645 y = np.arange(xy_range[0], xy_range[1] + 1, spacing)
invesalius/data/serial_port_connection.py 0 → 100644
@@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
  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 queue
  21 +import threading
  22 +import time
  23 +
  24 +import wx
  25 +from invesalius.pubsub import pub as Publisher
  26 +
  27 +
  28 +class SerialPortConnection(threading.Thread):
  29 + BINARY_PULSE = b'\x01'
  30 +
  31 + def __init__(self, port, serial_port_queue, event, sleep_nav):
  32 + """
  33 + Thread created to communicate using the serial port to interact with software during neuronavigation.
  34 + """
  35 + threading.Thread.__init__(self, name='Serial port')
  36 +
  37 + self.connection = None
  38 + self.stylusplh = False
  39 +
  40 + self.port = port
  41 + self.serial_port_queue = serial_port_queue
  42 + self.event = event
  43 + self.sleep_nav = sleep_nav
  44 +
  45 + def Connect(self):
  46 + if self.port is None:
  47 + print("Serial port init error: COM port is unset.")
  48 + return
  49 + try:
  50 + import serial
  51 + self.connection = serial.Serial(self.port, baudrate=115200, timeout=0)
  52 + print("Connection to port {} opened.".format(self.port))
  53 +
  54 + Publisher.sendMessage('Serial port connection', state=True)
  55 + except:
  56 + print("Serial port init error: Connecting to port {} failed.".format(self.port))
  57 +
  58 + def Disconnect(self):
  59 + if self.connection:
  60 + self.connection.close()
  61 + print("Connection to port {} closed.".format(self.port))
  62 +
  63 + Publisher.sendMessage('Serial port connection', state=False)
  64 +
  65 + def SendPulse(self):
  66 + try:
  67 + self.connection.write(self.BINARY_PULSE)
  68 + Publisher.sendMessage('Serial port pulse triggered')
  69 + except:
  70 + print("Error: Serial port could not be written into.")
  71 +
  72 + def run(self):
  73 + while not self.event.is_set():
  74 + trigger_on = False
  75 + try:
  76 + lines = self.connection.readlines()
  77 + except:
  78 + print("Error: Serial port could not be read.")
  79 +
  80 + if lines:
  81 + trigger_on = True
  82 +
  83 + if self.stylusplh:
  84 + trigger_on = True
  85 + self.stylusplh = False
  86 +
  87 + try:
  88 + self.serial_port_queue.put_nowait(trigger_on)
  89 + except queue.Full:
  90 + print("Error: Serial port queue full.")
  91 +
  92 + time.sleep(self.sleep_nav)
  93 +
  94 + # XXX: This is needed here because the serial port queue has to be read
  95 + # at least as fast as it is written into, otherwise it will eventually
  96 + # become full. Reading is done in another thread, which has the same
  97 + # sleeping parameter sleep_nav between consecutive runs as this thread.
  98 + # However, a single run of that thread takes longer to finish than a
  99 + # single run of this thread, causing that thread to lag behind. Hence,
  100 + # the additional sleeping here to ensure that this thread lags behind the
  101 + # other thread and not the other way around. However, it would be nice to
  102 + # handle the timing dependencies between the threads in a more robust way.
  103 + #
  104 + time.sleep(0.3)
  105 + else:
  106 + self.Disconnect()
invesalius/data/styles.py
@@ -2600,6 +2600,7 @@ class SelectMaskPartsInteractorStyle(DefaultInteractorStyle): @@ -2600,6 +2600,7 @@ class SelectMaskPartsInteractorStyle(DefaultInteractorStyle):
2600 class FFillSegmentationConfig(metaclass=utils.Singleton): 2600 class FFillSegmentationConfig(metaclass=utils.Singleton):
2601 def __init__(self): 2601 def __init__(self):
2602 self.dlg_visible = False 2602 self.dlg_visible = False
  2603 + self.dlg = None
2603 self.target = "2D" 2604 self.target = "2D"
2604 self.con_2d = 4 2605 self.con_2d = 4
2605 self.con_3d = 6 2606 self.con_3d = 6
@@ -2634,7 +2635,6 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): @@ -2634,7 +2635,6 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2634 self.slice_data = viewer.slice_data 2635 self.slice_data = viewer.slice_data
2635 2636
2636 self.config = FFillSegmentationConfig() 2637 self.config = FFillSegmentationConfig()
2637 - self.dlg_ffill = None  
2638 2638
2639 self._progr_title = _(u"Region growing") 2639 self._progr_title = _(u"Region growing")
2640 self._progr_msg = _(u"Segmenting ...") 2640 self._progr_msg = _(u"Segmenting ...")
@@ -2652,14 +2652,14 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): @@ -2652,14 +2652,14 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2652 self.config.t1 = int(_max) 2652 self.config.t1 = int(_max)
2653 2653
2654 self.config.dlg_visible = True 2654 self.config.dlg_visible = True
2655 - self.dlg_ffill = dialogs.FFillSegmentationOptionsDialog(self.config)  
2656 - self.dlg_ffill.Show() 2655 + self.config.dlg = dialogs.FFillSegmentationOptionsDialog(self.config)
  2656 + self.config.dlg.Show()
2657 2657
2658 def CleanUp(self): 2658 def CleanUp(self):
2659 - if (self.dlg_ffill is not None) and (self.config.dlg_visible): 2659 + if (self.config.dlg is not None) and (self.config.dlg_visible):
2660 self.config.dlg_visible = False 2660 self.config.dlg_visible = False
2661 - self.dlg_ffill.Destroy()  
2662 - self.dlg_ffill = None 2661 + self.config.dlg.Destroy()
  2662 + self.config.dlg = None
2663 2663
2664 def OnFFClick(self, obj, evt): 2664 def OnFFClick(self, obj, evt):
2665 if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): 2665 if (self.viewer.slice_.buffer_slices[self.orientation].mask is None):
@@ -2787,22 +2787,28 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): @@ -2787,22 +2787,28 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2787 with futures.ThreadPoolExecutor(max_workers=1) as executor: 2787 with futures.ThreadPoolExecutor(max_workers=1) as executor:
2788 future = executor.submit(self.do_rg_confidence, image, mask, (x, y, z), bstruct) 2788 future = executor.submit(self.do_rg_confidence, image, mask, (x, y, z), bstruct)
2789 2789
2790 - dlg = wx.ProgressDialog(self._progr_title, self._progr_msg, parent=wx.GetApp().GetTopWindow(), style=wx.PD_APP_MODAL|wx.PD_AUTO_HIDE) 2790 + self.config.dlg.panel_ffill_progress.Enable()
  2791 + self.config.dlg.panel_ffill_progress.StartTimer()
2791 while not future.done(): 2792 while not future.done():
2792 - dlg.Pulse() 2793 + self.config.dlg.panel_ffill_progress.Pulse()
  2794 + self.config.dlg.Update()
2793 time.sleep(0.1) 2795 time.sleep(0.1)
2794 - dlg.Destroy() 2796 + self.config.dlg.panel_ffill_progress.StopTimer()
  2797 + self.config.dlg.panel_ffill_progress.Disable()
2795 out_mask = future.result() 2798 out_mask = future.result()
2796 else: 2799 else:
2797 out_mask = np.zeros_like(mask) 2800 out_mask = np.zeros_like(mask)
2798 with futures.ThreadPoolExecutor(max_workers=1) as executor: 2801 with futures.ThreadPoolExecutor(max_workers=1) as executor:
2799 future = executor.submit(floodfill.floodfill_threshold, image, [[x, y, z]], t0, t1, 1, bstruct, out_mask) 2802 future = executor.submit(floodfill.floodfill_threshold, image, [[x, y, z]], t0, t1, 1, bstruct, out_mask)
2800 2803
2801 - dlg = wx.ProgressDialog(self._progr_title, self._progr_msg, parent=wx.GetApp().GetTopWindow(), style=wx.PD_APP_MODAL|wx.PD_AUTO_HIDE) 2804 + self.config.dlg.panel_ffill_progress.Enable()
  2805 + self.config.dlg.panel_ffill_progress.StartTimer()
2802 while not future.done(): 2806 while not future.done():
2803 - dlg.Pulse() 2807 + self.config.dlg.panel_ffill_progress.Pulse()
  2808 + self.config.dlg.Update()
2804 time.sleep(0.1) 2809 time.sleep(0.1)
2805 - dlg.Destroy() 2810 + self.config.dlg.panel_ffill_progress.StopTimer()
  2811 + self.config.dlg.panel_ffill_progress.Disable()
2806 2812
2807 mask[out_mask.astype('bool')] = self.config.fill_value 2813 mask[out_mask.astype('bool')] = self.config.fill_value
2808 2814
invesalius/data/trigger.py
@@ -1,168 +0,0 @@ @@ -1,168 +0,0 @@
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 -from time import sleep  
22 -  
23 -import wx  
24 -from invesalius.pubsub import pub as Publisher  
25 -  
26 -  
27 -class Trigger(threading.Thread):  
28 - """  
29 - Thread created to use external trigger to interact with software during neuronavigation  
30 - """  
31 -  
32 - def __init__(self, nav_id):  
33 - threading.Thread.__init__(self)  
34 - self.trigger_init = None  
35 - self.stylusplh = False  
36 - self.COM = False  
37 - self.nav_id = nav_id  
38 - self.__bind_events()  
39 - try:  
40 - import serial  
41 -  
42 - self.trigger_init = serial.Serial('COM5', baudrate=115200, timeout=0)  
43 - self.COM = True  
44 -  
45 - except:  
46 - #wx.MessageBox(_('Connection with port COM1 failed'), _('Communication error'), wx.OK | wx.ICON_ERROR)  
47 - print("Trigger init error: Connection with port COM1 failed")  
48 - self.COM = False  
49 -  
50 - self._pause_ = False  
51 - self.start()  
52 -  
53 - def __bind_events(self):  
54 - Publisher.subscribe(self.OnStylusPLH, 'PLH Stylus Button On')  
55 -  
56 - def OnStylusPLH(self):  
57 - self.stylusplh = True  
58 -  
59 - def stop(self):  
60 - self._pause_ = True  
61 -  
62 - def run(self):  
63 - while self.nav_id:  
64 - if self.COM:  
65 - self.trigger_init.write(b'0')  
66 - sleep(0.3)  
67 - lines = self.trigger_init.readlines()  
68 - # Following lines can simulate a trigger in 3 sec repetitions  
69 - # sleep(3)  
70 - # lines = True  
71 - if lines:  
72 - wx.CallAfter(Publisher.sendMessage, 'Create marker')  
73 - sleep(0.5)  
74 -  
75 - if self.stylusplh:  
76 - wx.CallAfter(Publisher.sendMessage, 'Create marker')  
77 - sleep(0.5)  
78 - self.stylusplh = False  
79 -  
80 - sleep(0.175)  
81 - if self._pause_:  
82 - if self.trigger_init:  
83 - self.trigger_init.close()  
84 - return  
85 -  
86 -  
87 -class TriggerNew(threading.Thread):  
88 -  
89 - def __init__(self, trigger_queue, event, sle):  
90 - """Class (threading) to compute real time tractography data for visualization in a single loop.  
91 -  
92 - Different than ComputeTractsThread because it does not keep adding tracts to the bundle until maximum,  
93 - is reached. It actually compute all requested tracts at once. (Might be deleted in the future)!  
94 - Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/)  
95 - For VTK visualization, each tract (fiber) is a constructed as a tube and many tubes combined in one  
96 - vtkMultiBlockDataSet named as a branch. Several branches are combined in another vtkMultiBlockDataSet named as  
97 - bundle, to obtain fast computation and visualization. The bundle dataset is mapped to a single vtkActor.  
98 - Mapper and Actor are computer in the data/viewer_volume.py module for easier handling in the invesalius 3D scene.  
99 -  
100 - Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation  
101 -  
102 - :param inp: List of inputs: trekker instance, affine numpy array, seed_offset, seed_radius, n_threads  
103 - :type inp: list  
104 - :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene  
105 - :type affine_vtk: vtkMatrix4x4  
106 - :param coord_queue: Queue instance that manage coordinates read from tracking device and coregistered  
107 - :type coord_queue: queue.Queue  
108 - :param visualization_queue: Queue instance that manage coordinates to be visualized  
109 - :type visualization_queue: queue.Queue  
110 - :param event: Threading event to coordinate when tasks as done and allow UI release  
111 - :type event: threading.Event  
112 - :param sle: Sleep pause in seconds  
113 - :type sle: float  
114 - """  
115 -  
116 - threading.Thread.__init__(self, name='Trigger')  
117 -  
118 - self.trigger_init = None  
119 - self.stylusplh = False  
120 - # self.COM = False  
121 - # self.__bind_events()  
122 - try:  
123 - import serial  
124 -  
125 - self.trigger_init = serial.Serial('COM5', baudrate=115200, timeout=0)  
126 - # self.COM = True  
127 -  
128 - except:  
129 - #wx.MessageBox(_('Connection with port COM1 failed'), _('Communication error'), wx.OK | wx.ICON_ERROR)  
130 - print("Trigger init error: Connection with port COM failed")  
131 - # self.COM = False  
132 - pass  
133 -  
134 - # self.coord_queue = coord_queue  
135 - self.trigger_queue = trigger_queue  
136 - self.event = event  
137 - self.sle = sle  
138 -  
139 - def run(self):  
140 -  
141 - while not self.event.is_set():  
142 - trigger_on = False  
143 - try:  
144 - self.trigger_init.write(b'0')  
145 - sleep(0.3)  
146 - lines = self.trigger_init.readlines()  
147 - # Following lines can simulate a trigger in 3 sec repetitions  
148 - # sleep(3)  
149 - # lines = True  
150 - if lines:  
151 - trigger_on = True  
152 - # wx.CallAfter(Publisher.sendMessage, 'Create marker')  
153 -  
154 - if self.stylusplh:  
155 - trigger_on = True  
156 - # wx.CallAfter(Publisher.sendMessage, 'Create marker')  
157 - self.stylusplh = False  
158 -  
159 - self.trigger_queue.put_nowait(trigger_on)  
160 - sleep(self.sle)  
161 -  
162 - except:  
163 - print("Trigger not read, error")  
164 - pass  
165 -  
166 - else:  
167 - if self.trigger_init:  
168 - self.trigger_init.close()  
invesalius/data/viewer_volume.py
@@ -36,6 +36,7 @@ from scipy.spatial import distance @@ -36,6 +36,7 @@ from scipy.spatial import distance
36 from imageio import imsave 36 from imageio import imsave
37 37
38 import invesalius.constants as const 38 import invesalius.constants as const
  39 +import invesalius.data.coordinates as dco
39 import invesalius.data.slice_ as sl 40 import invesalius.data.slice_ as sl
40 import invesalius.data.styles_3d as styles 41 import invesalius.data.styles_3d as styles
41 import invesalius.data.transformations as tr 42 import invesalius.data.transformations as tr
@@ -192,6 +193,7 @@ class Viewer(wx.Panel): @@ -192,6 +193,7 @@ class Viewer(wx.Panel):
192 self.dummy_coil_actor = None 193 self.dummy_coil_actor = None
193 self.target_mode = False 194 self.target_mode = False
194 self.polydata = None 195 self.polydata = None
  196 + self.use_default_object = True
195 self.anglethreshold = const.COIL_ANGLES_THRESHOLD 197 self.anglethreshold = const.COIL_ANGLES_THRESHOLD
196 self.distthreshold = const.COIL_COORD_THRESHOLD 198 self.distthreshold = const.COIL_COORD_THRESHOLD
197 self.angle_arrow_projection_threshold = const.COIL_ANGLE_ARROW_PROJECTION_THRESHOLD 199 self.angle_arrow_projection_threshold = const.COIL_ANGLE_ARROW_PROJECTION_THRESHOLD
@@ -278,7 +280,7 @@ class Viewer(wx.Panel): @@ -278,7 +280,7 @@ class Viewer(wx.Panel):
278 Publisher.subscribe(self.HideAllMarkers, 'Hide all markers') 280 Publisher.subscribe(self.HideAllMarkers, 'Hide all markers')
279 Publisher.subscribe(self.ShowAllMarkers, 'Show all markers') 281 Publisher.subscribe(self.ShowAllMarkers, 'Show all markers')
280 Publisher.subscribe(self.RemoveAllMarkers, 'Remove all markers') 282 Publisher.subscribe(self.RemoveAllMarkers, 'Remove all markers')
281 - Publisher.subscribe(self.RemoveMarker, 'Remove marker') 283 + Publisher.subscribe(self.RemoveMultipleMarkers, 'Remove multiple markers')
282 Publisher.subscribe(self.BlinkMarker, 'Blink Marker') 284 Publisher.subscribe(self.BlinkMarker, 'Blink Marker')
283 Publisher.subscribe(self.StopBlinkMarker, 'Stop Blink Marker') 285 Publisher.subscribe(self.StopBlinkMarker, 'Stop Blink Marker')
284 Publisher.subscribe(self.SetNewColor, 'Set new color') 286 Publisher.subscribe(self.SetNewColor, 'Set new color')
@@ -343,18 +345,23 @@ class Viewer(wx.Panel): @@ -343,18 +345,23 @@ class Viewer(wx.Panel):
343 if style == const.SLICE_STATE_CROSS: 345 if style == const.SLICE_STATE_CROSS:
344 self._mode_cross = True 346 self._mode_cross = True
345 # self._check_and_set_ball_visibility() 347 # self._check_and_set_ball_visibility()
  348 + #if not self.actor_peel:
346 self._ball_ref_visibility = True 349 self._ball_ref_visibility = True
  350 + #else:
  351 + # self._ball_ref_visibility = False
347 # if self._to_show_ball: 352 # if self._to_show_ball:
348 - if not self.ball_actor: 353 + if not self.ball_actor: #and not self.actor_peel:
349 self.CreateBallReference() 354 self.CreateBallReference()
350 - 355 + #self.ball_actor.SetVisibility(1)
  356 + #else:
  357 + # self.ball_actor.SetVisibility(0)
351 self.interactor.Render() 358 self.interactor.Render()
352 359
353 def _uncheck_ball_reference(self, style): 360 def _uncheck_ball_reference(self, style):
354 if style == const.SLICE_STATE_CROSS: 361 if style == const.SLICE_STATE_CROSS:
355 self._mode_cross = False 362 self._mode_cross = False
356 # self.RemoveBallReference() 363 # self.RemoveBallReference()
357 - self._ball_ref_visibility = False 364 + self._ball_ref_visibility = True
358 if self.ball_actor: 365 if self.ball_actor:
359 self.ren.RemoveActor(self.ball_actor) 366 self.ren.RemoveActor(self.ball_actor)
360 self.ball_actor = None 367 self.ball_actor = None
@@ -671,7 +678,7 @@ class Viewer(wx.Panel): @@ -671,7 +678,7 @@ class Viewer(wx.Panel):
671 self.staticballs = [] 678 self.staticballs = []
672 self.UpdateRender() 679 self.UpdateRender()
673 680
674 - def RemoveMarker(self, index): 681 + def RemoveMultipleMarkers(self, index):
675 for i in reversed(index): 682 for i in reversed(index):
676 self.ren.RemoveActor(self.staticballs[i]) 683 self.ren.RemoveActor(self.staticballs[i])
677 del self.staticballs[i] 684 del self.staticballs[i]
@@ -702,7 +709,7 @@ class Viewer(wx.Panel): @@ -702,7 +709,7 @@ class Viewer(wx.Panel):
702 self.index = False 709 self.index = False
703 710
704 def SetNewColor(self, index, color): 711 def SetNewColor(self, index, color):
705 - self.staticballs[index].GetProperty().SetColor(color) 712 + self.staticballs[index].GetProperty().SetColor([round(s/255.0, 3) for s in color])
706 self.Refresh() 713 self.Refresh()
707 714
708 def OnTargetMarkerTransparency(self, status, index): 715 def OnTargetMarkerTransparency(self, status, index):
@@ -980,10 +987,12 @@ class Viewer(wx.Panel): @@ -980,10 +987,12 @@ class Viewer(wx.Panel):
980 987
981 vtk_colors = vtk.vtkNamedColors() 988 vtk_colors = vtk.vtkNamedColors()
982 989
983 - a, b, g = np.radians(self.target_coord[3:])  
984 - r_ref = tr.euler_matrix(a, b, g, 'sxyz')  
985 - t_ref = tr.translation_matrix(self.target_coord[:3])  
986 - m_img = np.asmatrix(tr.concatenate_matrices(t_ref, r_ref)) 990 + m_img = dco.coordinates_to_transformation_matrix(
  991 + position=self.target_coord[:3],
  992 + orientation=self.target_coord[3:],
  993 + axes='sxyz',
  994 + )
  995 + m_img = np.asmatrix(m_img)
987 996
988 m_img_vtk = vtk.vtkMatrix4x4() 997 m_img_vtk = vtk.vtkMatrix4x4()
989 998
@@ -1019,10 +1028,10 @@ class Viewer(wx.Panel): @@ -1019,10 +1028,10 @@ class Viewer(wx.Panel):
1019 self.aim_actor = aim_actor 1028 self.aim_actor = aim_actor
1020 self.ren.AddActor(aim_actor) 1029 self.ren.AddActor(aim_actor)
1021 1030
1022 - if self.polydata:  
1023 - obj_polydata = self.polydata  
1024 - else: 1031 + if self.use_default_object:
1025 obj_polydata = self.CreateObjectPolyData(os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil_no_handle.stl")) 1032 obj_polydata = self.CreateObjectPolyData(os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil_no_handle.stl"))
  1033 + else:
  1034 + obj_polydata = self.polydata
1026 1035
1027 transform = vtk.vtkTransform() 1036 transform = vtk.vtkTransform()
1028 transform.RotateZ(90) 1037 transform.RotateZ(90)
@@ -1238,6 +1247,7 @@ class Viewer(wx.Panel): @@ -1238,6 +1247,7 @@ class Viewer(wx.Panel):
1238 # self.UpdateCameraBallPosition(None, position) 1247 # self.UpdateCameraBallPosition(None, position)
1239 1248
1240 def UpdateCameraBallPosition(self, position): 1249 def UpdateCameraBallPosition(self, position):
  1250 + #if not self.actor_peel:
1241 coord_flip = list(position[:3]) 1251 coord_flip = list(position[:3])
1242 coord_flip[1] = -coord_flip[1] 1252 coord_flip[1] = -coord_flip[1]
1243 self.ball_actor.SetPosition(coord_flip) 1253 self.ball_actor.SetPosition(coord_flip)
@@ -1328,6 +1338,9 @@ class Viewer(wx.Panel): @@ -1328,6 +1338,9 @@ class Viewer(wx.Panel):
1328 self.ren.AddActor(self.x_actor) 1338 self.ren.AddActor(self.x_actor)
1329 self.ren.AddActor(self.y_actor) 1339 self.ren.AddActor(self.y_actor)
1330 self.ren.AddActor(self.z_actor) 1340 self.ren.AddActor(self.z_actor)
  1341 + self.x_actor.SetVisibility(0)
  1342 + self.y_actor.SetVisibility(0)
  1343 + self.z_actor.SetVisibility(0)
1331 #self.ren.AddActor(self.obj_projection_arrow_actor) 1344 #self.ren.AddActor(self.obj_projection_arrow_actor)
1332 #self.ren.AddActor(self.object_orientation_torus_actor) 1345 #self.ren.AddActor(self.object_orientation_torus_actor)
1333 # self.obj_axes = vtk.vtkAxesActor() 1346 # self.obj_axes = vtk.vtkAxesActor()
@@ -1442,6 +1455,8 @@ class Viewer(wx.Panel): @@ -1442,6 +1455,8 @@ class Viewer(wx.Panel):
1442 self.ren.RemoveActor(self.object_orientation_torus_actor) 1455 self.ren.RemoveActor(self.object_orientation_torus_actor)
1443 self.ren.RemoveActor(self.obj_projection_arrow_actor) 1456 self.ren.RemoveActor(self.obj_projection_arrow_actor)
1444 self.actor_peel = None 1457 self.actor_peel = None
  1458 + self.ball_actor.SetVisibility(1)
  1459 +
1445 if flag and actor: 1460 if flag and actor:
1446 self.ren.AddActor(actor) 1461 self.ren.AddActor(actor)
1447 self.actor_peel = actor 1462 self.actor_peel = actor
@@ -1499,7 +1514,7 @@ class Viewer(wx.Panel): @@ -1499,7 +1514,7 @@ class Viewer(wx.Panel):
1499 1514
1500 self.ren.AddActor(self.obj_projection_arrow_actor) 1515 self.ren.AddActor(self.obj_projection_arrow_actor)
1501 self.ren.AddActor(self.object_orientation_torus_actor) 1516 self.ren.AddActor(self.object_orientation_torus_actor)
1502 - 1517 + self.ball_actor.SetVisibility(0)
1503 self.obj_projection_arrow_actor.SetPosition(closestPoint) 1518 self.obj_projection_arrow_actor.SetPosition(closestPoint)
1504 self.obj_projection_arrow_actor.SetOrientation(coil_dir) 1519 self.obj_projection_arrow_actor.SetOrientation(coil_dir)
1505 1520
@@ -1520,6 +1535,8 @@ class Viewer(wx.Panel): @@ -1520,6 +1535,8 @@ class Viewer(wx.Panel):
1520 self.ren.RemoveActor(self.obj_projection_arrow_actor) 1535 self.ren.RemoveActor(self.obj_projection_arrow_actor)
1521 self.ren.RemoveActor(self.object_orientation_torus_actor) 1536 self.ren.RemoveActor(self.object_orientation_torus_actor)
1522 self.ren.RemoveActor(self.x_actor) 1537 self.ren.RemoveActor(self.x_actor)
  1538 + self.ball_actor.SetVisibility(1)
  1539 +
1523 #self.ren.RemoveActor(self.y_actor) 1540 #self.ren.RemoveActor(self.y_actor)
1524 self.Refresh() 1541 self.Refresh()
1525 1542
@@ -1531,9 +1548,9 @@ class Viewer(wx.Panel): @@ -1531,9 +1548,9 @@ class Viewer(wx.Panel):
1531 self.pTarget = self.CenterOfMass() 1548 self.pTarget = self.CenterOfMass()
1532 if self.obj_actor: 1549 if self.obj_actor:
1533 self.obj_actor.SetVisibility(self.obj_state) 1550 self.obj_actor.SetVisibility(self.obj_state)
1534 - self.x_actor.SetVisibility(self.obj_state)  
1535 - self.y_actor.SetVisibility(self.obj_state)  
1536 - self.z_actor.SetVisibility(self.obj_state) 1551 + #self.x_actor.SetVisibility(self.obj_state)
  1552 + #self.y_actor.SetVisibility(self.obj_state)
  1553 + #self.z_actor.SetVisibility(self.obj_state)
1537 #self.object_orientation_torus_actor.SetVisibility(self.obj_state) 1554 #self.object_orientation_torus_actor.SetVisibility(self.obj_state)
1538 #self.obj_projection_arrow_actor.SetVisibility(self.obj_state) 1555 #self.obj_projection_arrow_actor.SetVisibility(self.obj_state)
1539 self.Refresh() 1556 self.Refresh()
@@ -1599,10 +1616,11 @@ class Viewer(wx.Panel): @@ -1599,10 +1616,11 @@ class Viewer(wx.Panel):
1599 self.GetCellIntersection(p1, norm, coil_norm, coil_dir) 1616 self.GetCellIntersection(p1, norm, coil_norm, coil_dir)
1600 self.Refresh() 1617 self.Refresh()
1601 1618
1602 - def UpdateTrackObjectState(self, evt=None, flag=None, obj_name=None, polydata=None): 1619 + def UpdateTrackObjectState(self, evt=None, flag=None, obj_name=None, polydata=None, use_default_object=True):
1603 if flag: 1620 if flag:
1604 self.obj_name = obj_name 1621 self.obj_name = obj_name
1605 self.polydata = polydata 1622 self.polydata = polydata
  1623 + self.use_default_object = use_default_object
1606 if not self.obj_actor: 1624 if not self.obj_actor:
1607 self.AddObjectActor(self.obj_name) 1625 self.AddObjectActor(self.obj_name)
1608 else: 1626 else:
@@ -1630,7 +1648,10 @@ class Viewer(wx.Panel): @@ -1630,7 +1648,10 @@ class Viewer(wx.Panel):
1630 self.x_actor.SetVisibility(self.obj_state) 1648 self.x_actor.SetVisibility(self.obj_state)
1631 self.y_actor.SetVisibility(self.obj_state) 1649 self.y_actor.SetVisibility(self.obj_state)
1632 self.z_actor.SetVisibility(self.obj_state) 1650 self.z_actor.SetVisibility(self.obj_state)
1633 - 1651 + #if self.actor_peel:
  1652 + # self.ball_actor.SetVisibility(0)
  1653 + #else:
  1654 + # self.ball_actor.SetVisibility(1)
1634 self.Refresh() 1655 self.Refresh()
1635 1656
1636 def OnUpdateTracts(self, root=None, affine_vtk=None, coord_offset=None): 1657 def OnUpdateTracts(self, root=None, affine_vtk=None, coord_offset=None):
invesalius/gui/dialogs.py
@@ -2563,6 +2563,41 @@ class PanelFFillConfidence(wx.Panel): @@ -2563,6 +2563,41 @@ class PanelFFillConfidence(wx.Panel):
2563 self.config.confid_iters = self.spin_iters.GetValue() 2563 self.config.confid_iters = self.spin_iters.GetValue()
2564 2564
2565 2565
  2566 +class PanelFFillProgress(wx.Panel):
  2567 + def __init__(self, parent, ID=-1, style=wx.TAB_TRAVERSAL|wx.NO_BORDER):
  2568 + wx.Panel.__init__(self, parent, ID, style=style)
  2569 + self._init_gui()
  2570 +
  2571 + def _init_gui(self):
  2572 + self.progress = wx.Gauge(self, -1)
  2573 + self.lbl_progress_caption = wx.StaticText(self, -1, _("Elapsed time:"))
  2574 + self.lbl_time = wx.StaticText(self, -1, _("00:00:00"))
  2575 +
  2576 + main_sizer = wx.BoxSizer(wx.VERTICAL)
  2577 + main_sizer.Add(self.progress, 0, wx.EXPAND | wx.ALL, 5)
  2578 + time_sizer = wx.BoxSizer(wx.HORIZONTAL)
  2579 + time_sizer.Add(self.lbl_progress_caption, 0, wx.EXPAND, 0)
  2580 + time_sizer.Add(self.lbl_time, 1, wx.EXPAND | wx.LEFT, 5)
  2581 + main_sizer.Add(time_sizer, 0, wx.EXPAND | wx.ALL, 5)
  2582 +
  2583 + self.SetSizer(main_sizer)
  2584 + main_sizer.Fit(self)
  2585 + main_sizer.SetSizeHints(self)
  2586 +
  2587 + def StartTimer(self):
  2588 + self.t0 = time.time()
  2589 +
  2590 + def StopTimer(self):
  2591 + fmt = "%H:%M:%S"
  2592 + self.lbl_time.SetLabel(time.strftime(fmt, time.gmtime(time.time() - self.t0)))
  2593 + self.progress.SetValue(0)
  2594 +
  2595 + def Pulse(self):
  2596 + fmt = "%H:%M:%S"
  2597 + self.lbl_time.SetLabel(time.strftime(fmt, time.gmtime(time.time() - self.t0)))
  2598 + self.progress.Pulse()
  2599 +
  2600 +
2566 class FFillOptionsDialog(wx.Dialog): 2601 class FFillOptionsDialog(wx.Dialog):
2567 def __init__(self, title, config): 2602 def __init__(self, title, config):
2568 wx.Dialog.__init__(self, wx.GetApp().GetTopWindow(), -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.STAY_ON_TOP) 2603 wx.Dialog.__init__(self, wx.GetApp().GetTopWindow(), -1, title, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.STAY_ON_TOP)
@@ -2820,6 +2855,10 @@ class FFillSegmentationOptionsDialog(wx.Dialog): @@ -2820,6 +2855,10 @@ class FFillSegmentationOptionsDialog(wx.Dialog):
2820 self.panel_ffill_confidence.SetMinSize((250, -1)) 2855 self.panel_ffill_confidence.SetMinSize((250, -1))
2821 self.panel_ffill_confidence.Hide() 2856 self.panel_ffill_confidence.Hide()
2822 2857
  2858 + self.panel_ffill_progress = PanelFFillProgress(self, -1, style=wx.TAB_TRAVERSAL)
  2859 + self.panel_ffill_progress.SetMinSize((250, -1))
  2860 + # self.panel_ffill_progress.Hide()
  2861 +
2823 self.close_btn = wx.Button(self, wx.ID_CLOSE) 2862 self.close_btn = wx.Button(self, wx.ID_CLOSE)
2824 2863
2825 # Sizer 2864 # Sizer
@@ -2876,11 +2915,16 @@ class FFillSegmentationOptionsDialog(wx.Dialog): @@ -2876,11 +2915,16 @@ class FFillSegmentationOptionsDialog(wx.Dialog):
2876 sizer.Add(0, 0, (12, 0)) 2915 sizer.Add(0, 0, (12, 0))
2877 except TypeError: 2916 except TypeError:
2878 sizer.AddStretchSpacer((12, 0)) 2917 sizer.AddStretchSpacer((12, 0))
2879 - sizer.Add(self.close_btn, (13, 0), (1, 6), flag=wx.ALIGN_RIGHT|wx.RIGHT, border=5) 2918 + sizer.Add(self.panel_ffill_progress, (13, 0), (1, 6), flag=wx.ALIGN_RIGHT|wx.RIGHT, border=5)
2880 try: 2919 try:
2881 sizer.Add(0, 0, (14, 0)) 2920 sizer.Add(0, 0, (14, 0))
2882 except TypeError: 2921 except TypeError:
2883 sizer.AddStretchSpacer((14, 0)) 2922 sizer.AddStretchSpacer((14, 0))
  2923 + sizer.Add(self.close_btn, (15, 0), (1, 6), flag=wx.ALIGN_RIGHT|wx.RIGHT, border=5)
  2924 + try:
  2925 + sizer.Add(0, 0, (16, 0))
  2926 + except TypeError:
  2927 + sizer.AddStretchSpacer((16, 0))
2884 2928
2885 self.SetSizer(sizer) 2929 self.SetSizer(sizer)
2886 sizer.Fit(self) 2930 sizer.Fit(self)
@@ -3263,14 +3307,17 @@ class MaskDensityDialog(wx.Dialog): @@ -3263,14 +3307,17 @@ class MaskDensityDialog(wx.Dialog):
3263 3307
3264 class ObjectCalibrationDialog(wx.Dialog): 3308 class ObjectCalibrationDialog(wx.Dialog):
3265 3309
3266 - def __init__(self, nav_prop): 3310 + def __init__(self, tracker, pedal_connection):
  3311 + self.tracker = tracker
  3312 + self.pedal_connection = pedal_connection
3267 3313
3268 - self.tracker_id = nav_prop[0]  
3269 - self.trk_init = nav_prop[1]  
3270 - self.TrackerCoordinates = nav_prop[2] 3314 + self.trk_init, self.tracker_id = tracker.GetTrackerInfo()
  3315 + #self.TrackerCoordinates = nav_prop[2]
3271 self.obj_ref_id = 2 3316 self.obj_ref_id = 2
3272 self.obj_name = None 3317 self.obj_name = None
3273 self.polydata = None 3318 self.polydata = None
  3319 + self.use_default_object = False
  3320 + self.object_fiducial_being_set = None
3274 3321
3275 self.obj_fiducials = np.full([5, 3], np.nan) 3322 self.obj_fiducials = np.full([5, 3], np.nan)
3276 self.obj_orients = np.full([5, 3], np.nan) 3323 self.obj_orients = np.full([5, 3], np.nan)
@@ -3281,6 +3328,11 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3281,6 +3328,11 @@ class ObjectCalibrationDialog(wx.Dialog):
3281 self._init_gui() 3328 self._init_gui()
3282 self.LoadObject() 3329 self.LoadObject()
3283 3330
  3331 + self.__bind_events()
  3332 +
  3333 + def __bind_events(self):
  3334 + Publisher.subscribe(self.SetObjectFiducial, 'Set object fiducial')
  3335 +
3284 def _init_gui(self): 3336 def _init_gui(self):
3285 self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) 3337 self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize())
3286 self.interactor.Enable(1) 3338 self.interactor.Enable(1)
@@ -3298,7 +3350,7 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3298,7 +3350,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3298 choice_ref = wx.ComboBox(self, -1, "", size=wx.Size(90, 23), 3350 choice_ref = wx.ComboBox(self, -1, "", size=wx.Size(90, 23),
3299 choices=const.REF_MODE, style=wx.CB_DROPDOWN | wx.CB_READONLY) 3351 choices=const.REF_MODE, style=wx.CB_DROPDOWN | wx.CB_READONLY)
3300 choice_ref.SetToolTip(tooltip) 3352 choice_ref.SetToolTip(tooltip)
3301 - choice_ref.Bind(wx.EVT_COMBOBOX, self.OnChoiceRefMode) 3353 + choice_ref.Bind(wx.EVT_COMBOBOX, self.OnChooseReferenceMode)
3302 choice_ref.SetSelection(1) 3354 choice_ref.SetSelection(1)
3303 choice_ref.Enable(1) 3355 choice_ref.Enable(1)
3304 if self.tracker_id == const.PATRIOT or self.tracker_id == const.ISOTRAKII: 3356 if self.tracker_id == const.PATRIOT or self.tracker_id == const.ISOTRAKII:
@@ -3331,15 +3383,17 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3331,15 +3383,17 @@ class ObjectCalibrationDialog(wx.Dialog):
3331 choice_sensor]) 3383 choice_sensor])
3332 3384
3333 # Push buttons for object fiducials 3385 # Push buttons for object fiducials
3334 - btns_obj = const.BTNS_OBJ  
3335 - tips_obj = const.TIPS_OBJ 3386 + for object_fiducial in const.OBJECT_FIDUCIALS:
  3387 + index = object_fiducial['fiducial_index']
  3388 + label = object_fiducial['label']
  3389 + button_id = object_fiducial['button_id']
  3390 + tip = object_fiducial['tip']
  3391 +
  3392 + ctrl = wx.ToggleButton(self, button_id, label=label, size=wx.Size(60, 23))
  3393 + ctrl.SetToolTip(wx.ToolTip(tip))
  3394 + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnObjectFiducialButton, index, ctrl=ctrl))
3336 3395
3337 - for k in btns_obj:  
3338 - n = list(btns_obj[k].keys())[0]  
3339 - lab = list(btns_obj[k].values())[0]  
3340 - self.btns_coord[n] = wx.Button(self, k, label=lab, size=wx.Size(60, 23))  
3341 - self.btns_coord[n].SetToolTip(wx.ToolTip(tips_obj[n]))  
3342 - self.btns_coord[n].Bind(wx.EVT_BUTTON, self.OnGetObjectFiducials) 3396 + self.btns_coord[index] = ctrl
3343 3397
3344 for m in range(0, 5): 3398 for m in range(0, 5):
3345 for n in range(0, 3): 3399 for n in range(0, 3):
@@ -3383,8 +3437,9 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3383,8 +3437,9 @@ class ObjectCalibrationDialog(wx.Dialog):
3383 return 0 3437 return 0
3384 3438
3385 def LoadObject(self): 3439 def LoadObject(self):
3386 - default = self.ObjectImportDialog()  
3387 - if not default: 3440 + self.use_default_object = self.ObjectImportDialog()
  3441 +
  3442 + if not self.use_default_object:
3388 filename = ShowImportMeshFilesDialog() 3443 filename = ShowImportMeshFilesDialog()
3389 3444
3390 if filename: 3445 if filename:
@@ -3397,11 +3452,17 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3397,11 +3452,17 @@ class ObjectCalibrationDialog(wx.Dialog):
3397 elif filename.lower().endswith('.vtp'): 3452 elif filename.lower().endswith('.vtp'):
3398 reader = vtk.vtkXMLPolyDataReader() 3453 reader = vtk.vtkXMLPolyDataReader()
3399 else: 3454 else:
3400 - wx.MessageBox(_("File format not reconized by InVesalius"), _("Import surface error")) 3455 + wx.MessageBox(_("File format not recognized by InVesalius"), _("Import surface error"))
3401 return 3456 return
3402 else: 3457 else:
3403 filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") 3458 filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl")
3404 reader = vtk.vtkSTLReader() 3459 reader = vtk.vtkSTLReader()
  3460 +
  3461 + # XXX: If the user cancels the dialog for importing the coil mesh file, the current behavior is to
  3462 + # use the default object after all. A more logical behavior in that case would be to cancel the
  3463 + # whole object calibration, but implementing that would need larger refactoring.
  3464 + #
  3465 + self.use_default_object = True
3405 else: 3466 else:
3406 filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") 3467 filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl")
3407 reader = vtk.vtkSTLReader() 3468 reader = vtk.vtkSTLReader()
@@ -3476,39 +3537,87 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3476,39 +3537,87 @@ class ObjectCalibrationDialog(wx.Dialog):
3476 self.ren.AddActor(ball_actor) 3537 self.ren.AddActor(ball_actor)
3477 return ball_actor, tactor 3538 return ball_actor, tactor
3478 3539
3479 - def OnGetObjectFiducials(self, evt):  
3480 - btn_id = list(const.BTNS_OBJ[evt.GetId()].keys())[0] 3540 + def OnObjectFiducialButton(self, index, evt, ctrl):
  3541 + if not self.tracker.IsTrackerInitialized():
  3542 + ShowNavigationTrackerWarning(0, 'choose')
  3543 + return
3481 3544
3482 - if self.trk_init and self.tracker_id:  
3483 - coord_raw, markers_flag = self.TrackerCoordinates.GetCoordinates()  
3484 - if self.obj_ref_id and btn_id == 4:  
3485 - if self.tracker_id == const.ROBOT:  
3486 - trck_init_robot = self.trk_init[1][0]  
3487 - coord = trck_init_robot.Run()  
3488 - else:  
3489 - coord = coord_raw[self.obj_ref_id, :] 3545 + # TODO: The code below until the end of the function is essentially copy-paste from
  3546 + # OnTrackerFiducials function in NeuronavigationPanel class. Probably the easiest
  3547 + # way to deduplicate this would be to create a Fiducial class, which would contain
  3548 + # this code just once.
  3549 + #
  3550 +
  3551 + # Do not allow several object fiducials to be set at the same time.
  3552 + if self.object_fiducial_being_set is not None and self.object_fiducial_being_set != index:
  3553 + ctrl.SetValue(False)
  3554 + return
  3555 +
  3556 + # Called when the button for setting the object fiducial is enabled and either pedal is pressed
  3557 + # or the button is pressed again.
  3558 + #
  3559 + def set_fiducial_callback(state):
  3560 + if state:
  3561 + Publisher.sendMessage('Set object fiducial', fiducial_index=index)
  3562 + if self.pedal_connection is not None:
  3563 + self.pedal_connection.remove_callback('fiducial')
  3564 +
  3565 + ctrl.SetValue(False)
  3566 + self.object_fiducial_being_set = None
  3567 +
  3568 + if ctrl.GetValue():
  3569 + self.object_fiducial_being_set = index
  3570 +
  3571 + if self.pedal_connection is not None:
  3572 + self.pedal_connection.add_callback('fiducial', set_fiducial_callback)
  3573 + else:
  3574 + set_fiducial_callback(True)
  3575 +
  3576 + def SetObjectFiducial(self, fiducial_index):
  3577 + coord, coord_raw = self.tracker.GetTrackerCoordinates(
  3578 + # XXX: Always use static reference mode when getting the coordinates. This is what the
  3579 + # code did previously, as well. At some point, it should probably be thought through
  3580 + # if this is actually what we want or if it should be changed somehow.
  3581 + #
  3582 + ref_mode_id=const.STATIC_REF,
  3583 + n_samples=const.CALIBRATION_TRACKER_SAMPLES,
  3584 + )
  3585 +
  3586 + # XXX: The condition below happens when setting the "fixed" coordinate in the object calibration.
  3587 + # The case is not handled by GetTrackerCoordinates function, therefore redo some computation
  3588 + # that is already done once by GetTrackerCoordinates, namely, invert the y-coordinate.
  3589 + #
  3590 + # (What is done here does not seem to be completely consistent with "always use static reference
  3591 + # mode" principle above, but it's hard to come up with a simple change to increase the consistency
  3592 + # and not change the function to the point of potentially breaking it.)
  3593 + #
  3594 + if self.obj_ref_id and fiducial_index == 4:
  3595 + if self.tracker_id == const.ROBOT:
  3596 + trck_init_robot = self.trk_init[1][0]
  3597 + coord = trck_init_robot.Run()
3490 else: 3598 else:
3491 - coord = coord_raw[0, :] 3599 + coord = coord_raw[self.obj_ref_id, :]
3492 else: 3600 else:
3493 - ShowNavigationTrackerWarning(0, 'choose') 3601 + coord = coord_raw[0, :]
  3602 + coord[2] = -coord[2]
3494 3603
3495 - if btn_id == 3: 3604 + if fiducial_index == 3:
3496 coord = np.zeros([6,]) 3605 coord = np.zeros([6,])
3497 3606
3498 # Update text controls with tracker coordinates 3607 # Update text controls with tracker coordinates
3499 if coord is not None or np.sum(coord) != 0.0: 3608 if coord is not None or np.sum(coord) != 0.0:
3500 - self.obj_fiducials[btn_id, :] = coord[:3]  
3501 - self.obj_orients[btn_id, :] = coord[3:]  
3502 - for n in [0, 1, 2]:  
3503 - self.txt_coord[btn_id][n].SetLabel(str(round(coord[n], 1)))  
3504 - if self.text_actors[btn_id]:  
3505 - self.text_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0)  
3506 - self.ball_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0) 3609 + self.obj_fiducials[fiducial_index, :] = coord[:3]
  3610 + self.obj_orients[fiducial_index, :] = coord[3:]
  3611 + for i in [0, 1, 2]:
  3612 + self.txt_coord[fiducial_index][i].SetLabel(str(round(coord[i], 1)))
  3613 + if self.text_actors[fiducial_index]:
  3614 + self.text_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0)
  3615 + self.ball_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0)
3507 self.Refresh() 3616 self.Refresh()
3508 else: 3617 else:
3509 ShowNavigationTrackerWarning(0, 'choose') 3618 ShowNavigationTrackerWarning(0, 'choose')
3510 3619
3511 - def OnChoiceRefMode(self, evt): 3620 + def OnChooseReferenceMode(self, evt):
3512 # When ref mode is changed the tracker coordinates are set to nan 3621 # When ref mode is changed the tracker coordinates are set to nan
3513 # This is for Polhemus FASTRAK wrapper, where the sensor attached to the object can be the stylus (Static 3622 # This is for Polhemus FASTRAK wrapper, where the sensor attached to the object can be the stylus (Static
3514 # reference - Selection 0 - index 0 for coordinates) or can be a 3rd sensor (Dynamic reference - Selection 1 - 3623 # reference - Selection 0 - index 0 for coordinates) or can be a 3rd sensor (Dynamic reference - Selection 1 -
@@ -3516,13 +3625,14 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3516,13 +3625,14 @@ class ObjectCalibrationDialog(wx.Dialog):
3516 # I use the index 2 directly here to send to the coregistration module where it is possible to access without 3625 # I use the index 2 directly here to send to the coregistration module where it is possible to access without
3517 # any conditional statement the correct index of coordinates. 3626 # any conditional statement the correct index of coordinates.
3518 3627
3519 - if evt.GetSelection(): 3628 + if evt.GetSelection() == 1:
3520 self.obj_ref_id = 2 3629 self.obj_ref_id = 2
3521 if self.tracker_id in [const.FASTRAK, const.DEBUGTRACKRANDOM, const.DEBUGTRACKAPPROACH]: 3630 if self.tracker_id in [const.FASTRAK, const.DEBUGTRACKRANDOM, const.DEBUGTRACKAPPROACH]:
3522 self.choice_sensor.Show(self.obj_ref_id) 3631 self.choice_sensor.Show(self.obj_ref_id)
3523 else: 3632 else:
3524 self.obj_ref_id = 0 3633 self.obj_ref_id = 0
3525 self.choice_sensor.Show(self.obj_ref_id) 3634 self.choice_sensor.Show(self.obj_ref_id)
  3635 +
3526 for m in range(0, 5): 3636 for m in range(0, 5):
3527 self.obj_fiducials[m, :] = np.full([1, 3], np.nan) 3637 self.obj_fiducials[m, :] = np.full([1, 3], np.nan)
3528 self.obj_orients[m, :] = np.full([1, 3], np.nan) 3638 self.obj_orients[m, :] = np.full([1, 3], np.nan)
@@ -3539,7 +3649,7 @@ class ObjectCalibrationDialog(wx.Dialog): @@ -3539,7 +3649,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3539 self.obj_ref_id = 0 3649 self.obj_ref_id = 0
3540 3650
3541 def GetValue(self): 3651 def GetValue(self):
3542 - return self.obj_fiducials, self.obj_orients, self.obj_ref_id, self.obj_name, self.polydata 3652 + return self.obj_fiducials, self.obj_orients, self.obj_ref_id, self.obj_name, self.polydata, self.use_default_object
3543 3653
3544 class ICPCorregistrationDialog(wx.Dialog): 3654 class ICPCorregistrationDialog(wx.Dialog):
3545 3655
invesalius/gui/task_navigator.py
@@ -17,11 +17,11 @@ @@ -17,11 +17,11 @@
17 # detalhes. 17 # detalhes.
18 #-------------------------------------------------------------------------- 18 #--------------------------------------------------------------------------
19 19
  20 +import dataclasses
20 from functools import partial 21 from functools import partial
  22 +import itertools
21 import csv 23 import csv
22 -import os  
23 import queue 24 import queue
24 -import sys  
25 import time 25 import time
26 import threading 26 import threading
27 27
@@ -42,10 +42,8 @@ except ImportError: @@ -42,10 +42,8 @@ except ImportError:
42 import wx 42 import wx
43 43
44 try: 44 try:
45 - import wx.lib.agw.hyperlink as hl  
46 import wx.lib.agw.foldpanelbar as fpb 45 import wx.lib.agw.foldpanelbar as fpb
47 except ImportError: 46 except ImportError:
48 - import wx.lib.hyperlink as hl  
49 import wx.lib.foldpanelbar as fpb 47 import wx.lib.foldpanelbar as fpb
50 48
51 import wx.lib.colourselect as csel 49 import wx.lib.colourselect as csel
@@ -54,23 +52,22 @@ from invesalius.pubsub import pub as Publisher @@ -54,23 +52,22 @@ from invesalius.pubsub import pub as Publisher
54 from time import sleep 52 from time import sleep
55 53
56 import invesalius.constants as const 54 import invesalius.constants as const
57 -import invesalius.data.bases as db  
58 55
59 if has_trekker: 56 if has_trekker:
60 import invesalius.data.brainmesh_handler as brain 57 import invesalius.data.brainmesh_handler as brain
61 58
62 -import invesalius.data.coordinates as dco  
63 -import invesalius.data.coregistration as dcr 59 +import invesalius.data.imagedata_utils as imagedata_utils
64 import invesalius.data.slice_ as sl 60 import invesalius.data.slice_ as sl
65 -import invesalius.data.trackers as dt  
66 import invesalius.data.tractography as dti 61 import invesalius.data.tractography as dti
67 -import invesalius.data.transformations as tr  
68 -import invesalius.data.trigger as trig  
69 import invesalius.data.record_coords as rec 62 import invesalius.data.record_coords as rec
70 import invesalius.data.vtk_utils as vtk_utils 63 import invesalius.data.vtk_utils as vtk_utils
71 import invesalius.gui.dialogs as dlg 64 import invesalius.gui.dialogs as dlg
72 import invesalius.project as prj 65 import invesalius.project as prj
73 from invesalius import utils 66 from invesalius import utils
  67 +from invesalius.gui import utils as gui_utils
  68 +from invesalius.navigation.icp import ICP
  69 +from invesalius.navigation.navigation import Navigation
  70 +from invesalius.navigation.tracker import Tracker
74 71
75 HAS_PEDAL_CONNECTION = True 72 HAS_PEDAL_CONNECTION = True
76 try: 73 try:
@@ -165,6 +162,12 @@ class InnerFoldPanel(wx.Panel): @@ -165,6 +162,12 @@ class InnerFoldPanel(wx.Panel):
165 162
166 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, 163 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
167 (10, 310), 0, fpb.FPB_SINGLE_FOLD) 164 (10, 310), 0, fpb.FPB_SINGLE_FOLD)
  165 +
  166 + # Initialize Tracker and PedalConnection objects here so that they are available to several panels.
  167 + #
  168 + tracker = Tracker()
  169 + pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None
  170 +
168 # Fold panel style 171 # Fold panel style
169 style = fpb.CaptionBarStyle() 172 style = fpb.CaptionBarStyle()
170 style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) 173 style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V)
@@ -173,7 +176,7 @@ class InnerFoldPanel(wx.Panel): @@ -173,7 +176,7 @@ class InnerFoldPanel(wx.Panel):
173 176
174 # Fold 1 - Navigation panel 177 # Fold 1 - Navigation panel
175 item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) 178 item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True)
176 - ntw = NeuronavigationPanel(item) 179 + ntw = NeuronavigationPanel(item, tracker, pedal_connection)
177 180
178 fold_panel.ApplyCaptionStyle(item, style) 181 fold_panel.ApplyCaptionStyle(item, style)
179 fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, 182 fold_panel.AddFoldPanelWindow(item, ntw, spacing=0,
@@ -182,7 +185,7 @@ class InnerFoldPanel(wx.Panel): @@ -182,7 +185,7 @@ class InnerFoldPanel(wx.Panel):
182 185
183 # Fold 2 - Object registration panel 186 # Fold 2 - Object registration panel
184 item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) 187 item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True)
185 - otw = ObjectRegistrationPanel(item) 188 + otw = ObjectRegistrationPanel(item, tracker, pedal_connection)
186 189
187 fold_panel.ApplyCaptionStyle(item, style) 190 fold_panel.ApplyCaptionStyle(item, style)
188 fold_panel.AddFoldPanelWindow(item, otw, spacing=0, 191 fold_panel.AddFoldPanelWindow(item, otw, spacing=0,
@@ -222,13 +225,13 @@ class InnerFoldPanel(wx.Panel): @@ -222,13 +225,13 @@ class InnerFoldPanel(wx.Panel):
222 checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCamera) 225 checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCamera)
223 self.checkcamera = checkcamera 226 self.checkcamera = checkcamera
224 227
225 - # Check box for trigger monitoring to create markers from serial port  
226 - tooltip = wx.ToolTip(_("Enable external trigger for creating markers"))  
227 - checktrigger = wx.CheckBox(self, -1, _('Ext. trigger'))  
228 - checktrigger.SetToolTip(tooltip)  
229 - checktrigger.SetValue(False)  
230 - checktrigger.Bind(wx.EVT_CHECKBOX, partial(self.OnExternalTrigger, ctrl=checktrigger))  
231 - self.checktrigger = checktrigger 228 + # Check box to create markers from serial port
  229 + tooltip = wx.ToolTip(_("Enable serial port communication for creating markers"))
  230 + checkbox_serial_port = wx.CheckBox(self, -1, _('Serial port'))
  231 + checkbox_serial_port.SetToolTip(tooltip)
  232 + checkbox_serial_port.SetValue(False)
  233 + checkbox_serial_port.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableSerialPort, ctrl=checkbox_serial_port))
  234 + self.checkbox_serial_port = checkbox_serial_port
232 235
233 # Check box for object position and orientation update in volume rendering during navigation 236 # Check box for object position and orientation update in volume rendering during navigation
234 tooltip = wx.ToolTip(_("Show and track TMS coil")) 237 tooltip = wx.ToolTip(_("Show and track TMS coil"))
@@ -241,12 +244,12 @@ class InnerFoldPanel(wx.Panel): @@ -241,12 +244,12 @@ class InnerFoldPanel(wx.Panel):
241 244
242 # if sys.platform != 'win32': 245 # if sys.platform != 'win32':
243 self.checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) 246 self.checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
244 - checktrigger.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) 247 + checkbox_serial_port.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
245 checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) 248 checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
246 249
247 line_sizer = wx.BoxSizer(wx.HORIZONTAL) 250 line_sizer = wx.BoxSizer(wx.HORIZONTAL)
248 line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) 251 line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5)
249 - line_sizer.Add(checktrigger, 0, wx.ALIGN_CENTER) 252 + line_sizer.Add(checkbox_serial_port, 0, wx.ALIGN_CENTER)
250 line_sizer.Add(checkobj, 0, wx.RIGHT | wx.LEFT, 5) 253 line_sizer.Add(checkobj, 0, wx.RIGHT | wx.LEFT, 5)
251 line_sizer.Fit(self) 254 line_sizer.Fit(self)
252 255
@@ -277,17 +280,24 @@ class InnerFoldPanel(wx.Panel): @@ -277,17 +280,24 @@ class InnerFoldPanel(wx.Panel):
277 280
278 def OnCheckStatus(self, nav_status, vis_status): 281 def OnCheckStatus(self, nav_status, vis_status):
279 if nav_status: 282 if nav_status:
280 - self.checktrigger.Enable(False) 283 + self.checkbox_serial_port.Enable(False)
281 self.checkobj.Enable(False) 284 self.checkobj.Enable(False)
282 else: 285 else:
283 - self.checktrigger.Enable(True) 286 + self.checkbox_serial_port.Enable(True)
284 if self.track_obj: 287 if self.track_obj:
285 self.checkobj.Enable(True) 288 self.checkobj.Enable(True)
286 289
287 - def OnExternalTrigger(self, evt, ctrl):  
288 - Publisher.sendMessage('Update trigger state', trigger_state=ctrl.GetValue()) 290 + def OnEnableSerialPort(self, evt, ctrl):
  291 + com_port = None
  292 + if ctrl.GetValue():
  293 + from wx import ID_OK
  294 + dlg_port = dlg.SetCOMport()
  295 + if dlg_port.ShowModal() == ID_OK:
  296 + com_port = dlg_port.GetValue()
289 297
290 - def OnShowObject(self, evt=None, flag=None, obj_name=None, polydata=None): 298 + Publisher.sendMessage('Update serial port', serial_port=com_port)
  299 +
  300 + def OnShowObject(self, evt=None, flag=None, obj_name=None, polydata=None, use_default_object=True):
291 if not evt: 301 if not evt:
292 if flag: 302 if flag:
293 self.checkobj.Enable(True) 303 self.checkobj.Enable(True)
@@ -308,432 +318,8 @@ class InnerFoldPanel(wx.Panel): @@ -308,432 +318,8 @@ class InnerFoldPanel(wx.Panel):
308 Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue()) 318 Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue())
309 319
310 320
311 -class Navigation():  
312 - def __init__(self):  
313 - self.image_fiducials = np.full([3, 3], np.nan)  
314 - self.correg = None  
315 - self.current_coord = 0, 0, 0  
316 - self.target = None  
317 - self.trigger = None  
318 - self.trigger_state = False  
319 - self.obj_reg = None  
320 - self.track_obj = False  
321 - self.m_change = None  
322 - self.all_fiducials = np.zeros((6, 6))  
323 -  
324 - self.event = threading.Event()  
325 -  
326 - self.coord_queue = QueueCustom(maxsize=1)  
327 - self.objattarget_queue = QueueCustom(maxsize=1)  
328 - self.icp_queue = QueueCustom(maxsize=1)  
329 - self.robottarget_queue = QueueCustom(maxsize=1)  
330 - # self.visualization_queue = QueueCustom(maxsize=1)  
331 - self.trigger_queue = QueueCustom(maxsize=1)  
332 - self.coord_tracts_queue = QueueCustom(maxsize=1)  
333 - self.tracts_queue = QueueCustom(maxsize=1)  
334 -  
335 - # Tractography parameters  
336 - self.trk_inp = None  
337 - self.trekker = None  
338 - self.n_threads = None  
339 - self.view_tracts = False  
340 - self.peel_loaded = False  
341 - self.enable_act = False  
342 - self.act_data = None  
343 - self.n_tracts = const.N_TRACTS  
344 - self.seed_offset = const.SEED_OFFSET  
345 - self.seed_radius = const.SEED_RADIUS  
346 - self.sleep_nav = const.SLEEP_NAVIGATION  
347 -  
348 - def SetImageFiducial(self, fiducial_index, coord):  
349 - self.image_fiducials[fiducial_index, :] = coord  
350 -  
351 - print("Set image fiducial {} to coordinates {}".format(fiducial_index, coord))  
352 -  
353 - def AreImageFiducialsSet(self):  
354 - return not np.isnan(self.image_fiducials).any()  
355 -  
356 - def UpdateFiducialRegistrationError(self, tracker):  
357 - tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()  
358 - ref_mode_id = tracker.GetReferenceMode()  
359 -  
360 - self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])  
361 -  
362 - self.fre = db.calculate_fre(tracker_fiducials_raw, self.all_fiducials, ref_mode_id, self.m_change)  
363 -  
364 - def GetFiducialRegistrationError(self, icp):  
365 - fre = icp.icp_fre if icp.use_icp else self.fre  
366 - return fre, fre <= const.FIDUCIAL_REGISTRATION_ERROR_THRESHOLD  
367 -  
368 - def StartNavigation(self, tracker):  
369 - tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()  
370 - ref_mode_id = tracker.GetReferenceMode()  
371 -  
372 - # initialize jobs list  
373 - jobs_list = []  
374 -  
375 - if self.event.is_set():  
376 - self.event.clear()  
377 -  
378 - vis_components = [self.trigger_state, self.view_tracts, self.peel_loaded]  
379 - vis_queues = [self.coord_queue, self.trigger_queue, self.tracts_queue, self.icp_queue, self.robottarget_queue]  
380 -  
381 - Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components)  
382 -  
383 - self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])  
384 -  
385 - # fiducials matrix  
386 - m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T,  
387 - shear=False, scale=False)  
388 - self.m_change = m_change  
389 -  
390 - errors = False  
391 -  
392 - if self.track_obj:  
393 - # if object tracking is selected  
394 - if self.obj_reg is None:  
395 - # check if object registration was performed  
396 - wx.MessageBox(_("Perform coil registration before navigation."), _("InVesalius 3"))  
397 - errors = True  
398 - else:  
399 - # if object registration was correctly performed continue with navigation  
400 - # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix  
401 - obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg  
402 -  
403 - coreg_data = [m_change, obj_ref_mode]  
404 -  
405 - if ref_mode_id:  
406 - coord_raw, markers_flag = tracker.TrackerCoordinates.GetCoordinates()  
407 - else:  
408 - coord_raw = np.array([None])  
409 -  
410 - obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change)  
411 - coreg_data.extend(obj_data)  
412 -  
413 - queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue, self.objattarget_queue]  
414 - jobs_list.append(dcr.CoordinateCorregistrate(ref_mode_id, tracker, coreg_data,  
415 - self.view_tracts, queues,  
416 - self.event, self.sleep_nav, tracker.tracker_id,  
417 - self.target))  
418 - else:  
419 - coreg_data = (m_change, 0)  
420 - queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue]  
421 - jobs_list.append(dcr.CoordinateCorregistrateNoObject(ref_mode_id, tracker, coreg_data,  
422 - self.view_tracts, queues,  
423 - self.event, self.sleep_nav))  
424 -  
425 - if not errors:  
426 - #TODO: Test the trigger thread  
427 - if self.trigger_state:  
428 - # self.trigger = trig.Trigger(nav_id)  
429 - jobs_list.append(trig.TriggerNew(self.trigger_queue, self.event, self.sleep_nav))  
430 -  
431 - if self.view_tracts:  
432 - # initialize Trekker parameters  
433 - slic = sl.Slice()  
434 - prj_data = prj.Project()  
435 - matrix_shape = tuple(prj_data.matrix_shape)  
436 - affine = slic.affine.copy()  
437 - affine[1, -1] -= matrix_shape[1]  
438 - affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine)  
439 - Publisher.sendMessage("Update marker offset state", create=True)  
440 - self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius,\  
441 - self.n_threads, self.act_data, affine_vtk, matrix_shape[1]  
442 - # print("Appending the tract computation thread!")  
443 - queues = [self.coord_tracts_queue, self.tracts_queue]  
444 - if self.enable_act:  
445 - jobs_list.append(dti.ComputeTractsACTThread(self.trk_inp, queues, self.event, self.sleep_nav))  
446 - else:  
447 - jobs_list.append(dti.ComputeTractsThread(self.trk_inp, queues, self.event, self.sleep_nav))  
448 -  
449 - jobs_list.append(UpdateNavigationScene(vis_queues, vis_components,  
450 - self.event, self.sleep_nav))  
451 -  
452 - for jobs in jobs_list:  
453 - # jobs.daemon = True  
454 - jobs.start()  
455 - # del jobs  
456 -  
457 - def StopNavigation(self):  
458 - self.event.set()  
459 -  
460 - self.coord_queue.clear()  
461 - self.coord_queue.join()  
462 -  
463 - if self.trigger_state:  
464 - self.trigger_queue.clear()  
465 - self.trigger_queue.join()  
466 - if self.view_tracts:  
467 - self.coord_tracts_queue.clear()  
468 - self.coord_tracts_queue.join()  
469 -  
470 - self.tracts_queue.clear()  
471 - self.tracts_queue.join()  
472 -  
473 - vis_components = [self.trigger_state, self.view_tracts, self.peel_loaded]  
474 - Publisher.sendMessage("Navigation status", nav_status=False, vis_status=vis_components)  
475 -  
476 -class Tracker():  
477 - def __init__(self):  
478 - self.trk_init = None  
479 - self.tracker_id = const.DEFAULT_TRACKER  
480 - self.ref_mode_id = const.DEFAULT_REF_MODE  
481 -  
482 - self.tracker_fiducials = np.full([3, 3], np.nan)  
483 - self.tracker_fiducials_raw = np.zeros((6, 6))  
484 - self.m_tracker_fiducials_raw = np.zeros((6, 4, 4))  
485 -  
486 - self.tracker_connected = False  
487 -  
488 - self.event_coord = threading.Event()  
489 - self.event_robot = threading.Event()  
490 - self.TrackerCoordinates = dco.TrackerCoordinates()  
491 -  
492 - def SetTracker(self, new_tracker):  
493 - if new_tracker:  
494 - self.DisconnectTracker()  
495 -  
496 - self.trk_init = dt.TrackerConnection(new_tracker, None, 'connect')  
497 - if not self.trk_init[0]:  
498 - dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1])  
499 -  
500 - self.tracker_id = 0  
501 - self.tracker_connected = False  
502 - else:  
503 - self.tracker_id = new_tracker  
504 - self.tracker_connected = True  
505 - dco.ReceiveCoordinates(self.trk_init, self.tracker_id, self.TrackerCoordinates, self.event_coord).start()  
506 -  
507 - Publisher.sendMessage('Update tracker initializer',  
508 - nav_prop=(self.tracker_id, self.trk_init, self.TrackerCoordinates, self.ref_mode_id))  
509 -  
510 - def DisconnectTracker(self):  
511 - if self.tracker_connected:  
512 - self.ResetTrackerFiducials()  
513 - Publisher.sendMessage('Update status text in GUI',  
514 - label=_("Disconnecting tracker ..."))  
515 - Publisher.sendMessage('Remove sensors ID')  
516 - Publisher.sendMessage('Remove object data')  
517 - self.trk_init = dt.TrackerConnection(self.tracker_id, self.trk_init[0], 'disconnect')  
518 -  
519 - if not self.trk_init[0]:  
520 - self.tracker_connected = False  
521 - self.tracker_id = 0  
522 -  
523 - self.event_coord.set()  
524 - self.event_robot.set()  
525 - # if self.event_coord.is_set():  
526 - # self.event_coord.clear()  
527 -  
528 - Publisher.sendMessage('Update status text in GUI',  
529 - label=_("Tracker disconnected"))  
530 - print("Tracker disconnected!")  
531 - else:  
532 - Publisher.sendMessage('Update status text in GUI',  
533 - label=_("Tracker still connected"))  
534 - print("Tracker still connected!")  
535 -  
536 - def IsTrackerInitialized(self):  
537 - return self.trk_init and self.tracker_id and self.tracker_connected  
538 -  
539 - def AreTrackerFiducialsSet(self):  
540 - return not np.isnan(self.tracker_fiducials).any()  
541 -  
542 - def SetTrackerFiducial(self, fiducial_index):  
543 - coord = None  
544 -  
545 - coord_raw, markers_flag = self.TrackerCoordinates.GetCoordinates()  
546 -  
547 - if self.ref_mode_id:  
548 - coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :])  
549 - else:  
550 - coord = coord_raw[0, :]  
551 - coord[2] = -coord[2]  
552 -  
553 - # Update tracker fiducial with tracker coordinates  
554 - self.tracker_fiducials[fiducial_index, :] = coord[0:3]  
555 -  
556 - assert 0 <= fiducial_index <= 2, "Fiducial index out of range (0-2): {}".format(fiducial_index)  
557 -  
558 - self.tracker_fiducials_raw[2 * fiducial_index, :] = coord_raw[0, :]  
559 - self.tracker_fiducials_raw[2 * fiducial_index + 1, :] = coord_raw[1, :]  
560 -  
561 - self.m_tracker_fiducials_raw[2 * fiducial_index, :] = dcr.compute_marker_transformation(coord_raw, 0)  
562 - self.m_tracker_fiducials_raw[2 * fiducial_index + 1, :] = dcr.compute_marker_transformation(coord_raw, 1)  
563 -  
564 - print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3]))  
565 -  
566 - def ResetTrackerFiducials(self):  
567 - for m in range(3):  
568 - self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan]  
569 -  
570 - def GetTrackerFiducials(self):  
571 - return self.tracker_fiducials, self.tracker_fiducials_raw  
572 -  
573 - def GetMatrixTrackerFiducials(self):  
574 - m_probe_ref_left = np.linalg.inv(self.m_tracker_fiducials_raw[1]) @ self.m_tracker_fiducials_raw[0]  
575 - m_probe_ref_right = np.linalg.inv(self.m_tracker_fiducials_raw[3]) @ self.m_tracker_fiducials_raw[2]  
576 - m_probe_ref_nasion = np.linalg.inv(self.m_tracker_fiducials_raw[5]) @ self.m_tracker_fiducials_raw[4]  
577 -  
578 - return [m_probe_ref_left, m_probe_ref_right, m_probe_ref_nasion]  
579 -  
580 - def GetTrackerInfo(self):  
581 - return self.trk_init, self.tracker_id, self.ref_mode_id  
582 -  
583 - def SetReferenceMode(self, value):  
584 - self.ref_mode_id = value  
585 -  
586 - # When ref mode is changed the tracker coordinates are set to zero  
587 - self.ResetTrackerFiducials()  
588 -  
589 - # Some trackers do not accept restarting within this time window  
590 - # TODO: Improve the restarting of trackers after changing reference mode  
591 - Publisher.sendMessage('Update tracker initializer',  
592 - nav_prop=(self.tracker_id, self.trk_init, self.TrackerCoordinates, self.ref_mode_id))  
593 -  
594 - def GetReferenceMode(self):  
595 - return self.ref_mode_id  
596 -  
597 - def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre):  
598 - if self.tracker_connected:  
599 - selection_ctrl.SetSelection(self.tracker_id)  
600 - else:  
601 - selection_ctrl.SetSelection(0)  
602 -  
603 - # Update tracker location in the UI.  
604 - for m in range(3):  
605 - coord = self.tracker_fiducials[m, :]  
606 - for n in range(0, 3):  
607 - value = 0.0 if np.isnan(coord[n]) else float(coord[n])  
608 - numctrls_fiducial[m][n].SetValue(value)  
609 -  
610 - txtctrl_fre.SetValue('')  
611 - txtctrl_fre.SetBackgroundColour('WHITE')  
612 -  
613 - def get_trackers(self):  
614 - return const.TRACKERS  
615 -  
616 -class ICP():  
617 - def __init__(self):  
618 - self.use_icp = False  
619 - self.m_icp = None  
620 - self.icp_fre = None  
621 -  
622 - def StartICP(self, navigation, tracker):  
623 - if not self.use_icp:  
624 - if dlg.ICPcorregistration(navigation.fre):  
625 - Publisher.sendMessage('Stop navigation')  
626 - use_icp, self.m_icp = self.OnICP(tracker, navigation.m_change)  
627 - if use_icp:  
628 - self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials,  
629 - tracker.ref_mode_id, navigation.m_change, self.m_icp)  
630 - self.SetICP(navigation, use_icp)  
631 - else:  
632 - print("ICP canceled")  
633 - Publisher.sendMessage('Start navigation')  
634 -  
635 - def OnICP(self, tracker, m_change):  
636 - ref_mode_id = tracker.GetReferenceMode()  
637 -  
638 - dialog = dlg.ICPCorregistrationDialog(nav_prop=(m_change, tracker.tracker_id, tracker.trk_init, ref_mode_id))  
639 -  
640 - if dialog.ShowModal() == wx.ID_OK:  
641 - m_icp, point_coord, transformed_points, prev_error, final_error = dialog.GetValue()  
642 - # TODO: checkbox in the dialog to transfer the icp points to 3D viewer  
643 - #create markers  
644 - # for i in range(len(point_coord)):  
645 - # img_coord = point_coord[i][0],-point_coord[i][1],point_coord[i][2], 0, 0, 0  
646 - # transf_coord = transformed_points[i][0],-transformed_points[i][1],transformed_points[i][2], 0, 0, 0  
647 - # Publisher.sendMessage('Create marker', coord=img_coord, marker_id=None, colour=(1,0,0))  
648 - # Publisher.sendMessage('Create marker', coord=transf_coord, marker_id=None, colour=(0,0,1))  
649 - if m_icp is not None:  
650 - dlg.ReportICPerror(prev_error, final_error)  
651 - use_icp = True  
652 - else:  
653 - use_icp = False  
654 -  
655 - return use_icp, m_icp  
656 -  
657 - else:  
658 - return self.use_icp, self.m_icp  
659 -  
660 - def SetICP(self, navigation, use_icp):  
661 - self.use_icp = use_icp  
662 - navigation.icp_queue.put_nowait([self.use_icp, self.m_icp])  
663 -  
664 - def ResetICP(self):  
665 - self.use_icp = False  
666 - self.m_icp = None  
667 - self.icp_fre = None  
668 -  
669 -class Robot():  
670 - def __init__(self):  
671 - self.trk_init = None  
672 - self.robottarget_queue = None  
673 - self.objattarget_queue = None  
674 - self.process_tracker = None  
675 -  
676 - self.__bind_events()  
677 -  
678 - def __bind_events(self):  
679 - Publisher.subscribe(self.OnSendCoordinates, 'Send coord to robot')  
680 - Publisher.subscribe(self.OnUpdateRobotTargetMatrix, 'Robot target matrix')  
681 - Publisher.subscribe(self.OnObjectTarget, 'Coil at target')  
682 -  
683 - def OnRobotConnection(self, tracker, robotcoordinates):  
684 - if not tracker.trk_init[0][0] or not tracker.trk_init[1][0]:  
685 - dlg.ShowNavigationTrackerWarning(tracker.tracker_id, tracker.trk_init[1])  
686 - tracker.tracker_id = 0  
687 - tracker.tracker_connected = False  
688 - else:  
689 - tracker.trk_init.append(robotcoordinates)  
690 - self.process_tracker = elfin_process.TrackerProcessing()  
691 - dlg_correg_robot = dlg.CreateTransformationMatrixRobot(tracker)  
692 - if dlg_correg_robot.ShowModal() == wx.ID_OK:  
693 - M_tracker_2_robot = dlg_correg_robot.GetValue()  
694 - db.transform_tracker_2_robot.M_tracker_2_robot = M_tracker_2_robot  
695 - self.robot_server = tracker.trk_init[1][0]  
696 - self.trk_init = tracker.trk_init  
697 - else:  
698 - dlg.ShowNavigationTrackerWarning(tracker.tracker_id, 'disconnect')  
699 - tracker.trk_init = None  
700 - tracker.tracker_id = 0  
701 - tracker.tracker_connected = False  
702 -  
703 - Publisher.sendMessage('Update tracker initializer',  
704 - nav_prop=(tracker.tracker_id, tracker.trk_init, tracker.TrackerCoordinates, tracker.ref_mode_id))  
705 -  
706 - def StartRobotNavigation(self, tracker, robotcoordinates, coord_queue):  
707 - if tracker.event_robot.is_set():  
708 - tracker.event_robot.clear()  
709 - elfin_process.ControlRobot(self.trk_init, tracker, robotcoordinates,  
710 - [coord_queue, self.robottarget_queue,  
711 - self.objattarget_queue],  
712 - self.process_tracker, tracker.event_robot).start()  
713 -  
714 - def OnSendCoordinates(self, coord):  
715 - self.robot_server.SendCoordinates(coord)  
716 -  
717 - def OnUpdateRobotTargetMatrix(self, robot_tracker_flag, m_change_robot2ref):  
718 - try:  
719 - self.robottarget_queue.put_nowait([robot_tracker_flag, m_change_robot2ref])  
720 - except queue.Full:  
721 - print('full target')  
722 - pass  
723 -  
724 - def OnObjectTarget(self, state):  
725 - try:  
726 - if self.objattarget_queue:  
727 - self.objattarget_queue.put_nowait(state)  
728 - except queue.Full:  
729 - #print('full flag target')  
730 - pass  
731 -  
732 - def SetRobotQueues(self, queues):  
733 - self.robottarget_queue, self.objattarget_queue = queues  
734 -  
735 class NeuronavigationPanel(wx.Panel): 321 class NeuronavigationPanel(wx.Panel):
736 - def __init__(self, parent): 322 + def __init__(self, parent, tracker, pedal_connection):
737 wx.Panel.__init__(self, parent) 323 wx.Panel.__init__(self, parent)
738 try: 324 try:
739 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) 325 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
@@ -746,14 +332,18 @@ class NeuronavigationPanel(wx.Panel): @@ -746,14 +332,18 @@ class NeuronavigationPanel(wx.Panel):
746 self.__bind_events() 332 self.__bind_events()
747 333
748 # Initialize global variables 334 # Initialize global variables
749 - self.pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None  
750 - self.tracker = Tracker()  
751 - self.navigation = Navigation() 335 + self.pedal_connection = pedal_connection
  336 + self.navigation = Navigation(
  337 + pedal_connection=pedal_connection,
  338 + )
752 self.icp = ICP() 339 self.icp = ICP()
  340 + self.tracker = tracker
753 self.robot = Robot() 341 self.robot = Robot()
754 self.robotcoordinates = elfin_process.RobotCoordinates() 342 self.robotcoordinates = elfin_process.RobotCoordinates()
755 343
756 self.nav_status = False 344 self.nav_status = False
  345 + self.tracker_fiducial_being_set = None
  346 + self.current_coord = 0, 0, 0
757 347
758 # Initialize list of buttons and numctrls for wx objects 348 # Initialize list of buttons and numctrls for wx objects
759 self.btns_set_fiducial = [None, None, None, None, None, None] 349 self.btns_set_fiducial = [None, None, None, None, None, None]
@@ -761,7 +351,7 @@ class NeuronavigationPanel(wx.Panel): @@ -761,7 +351,7 @@ class NeuronavigationPanel(wx.Panel):
761 351
762 # ComboBox for spatial tracker device selection 352 # ComboBox for spatial tracker device selection
763 tracker_options = [_("Select tracker:")] + self.tracker.get_trackers() 353 tracker_options = [_("Select tracker:")] + self.tracker.get_trackers()
764 - select_tracker_elem = wx.ComboBox(self, -1, "", size = (145,-1), 354 + select_tracker_elem = wx.ComboBox(self, -1, "", size=(145, -1),
765 choices=tracker_options, style=wx.CB_DROPDOWN|wx.CB_READONLY) 355 choices=tracker_options, style=wx.CB_DROPDOWN|wx.CB_READONLY)
766 356
767 tooltip = wx.ToolTip(_("Choose the tracking device")) 357 tooltip = wx.ToolTip(_("Choose the tracking device"))
@@ -786,9 +376,12 @@ class NeuronavigationPanel(wx.Panel): @@ -786,9 +376,12 @@ class NeuronavigationPanel(wx.Panel):
786 label = fiducial['label'] 376 label = fiducial['label']
787 tip = fiducial['tip'] 377 tip = fiducial['tip']
788 378
789 - self.btns_set_fiducial[n] = wx.ToggleButton(self, button_id, label=label, size=wx.Size(45, 23))  
790 - self.btns_set_fiducial[n].SetToolTip(wx.ToolTip(tip))  
791 - self.btns_set_fiducial[n].Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnImageFiducials, n)) 379 + ctrl = wx.ToggleButton(self, button_id, label=label)
  380 + ctrl.SetMinSize((gui_utils.calc_width_needed(ctrl, 3), -1))
  381 + ctrl.SetToolTip(wx.ToolTip(tip))
  382 + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnImageFiducials, n))
  383 +
  384 + self.btns_set_fiducial[n] = ctrl
792 385
793 # Push buttons for tracker fiducials 386 # Push buttons for tracker fiducials
794 for n, fiducial in enumerate(const.TRACKER_FIDUCIALS): 387 for n, fiducial in enumerate(const.TRACKER_FIDUCIALS):
@@ -796,15 +389,18 @@ class NeuronavigationPanel(wx.Panel): @@ -796,15 +389,18 @@ class NeuronavigationPanel(wx.Panel):
796 label = fiducial['label'] 389 label = fiducial['label']
797 tip = fiducial['tip'] 390 tip = fiducial['tip']
798 391
799 - self.btns_set_fiducial[n + 3] = wx.Button(self, button_id, label=label, size=wx.Size(45, 23))  
800 - self.btns_set_fiducial[n + 3].SetToolTip(wx.ToolTip(tip))  
801 - self.btns_set_fiducial[n + 3].Bind(wx.EVT_BUTTON, partial(self.OnTrackerFiducials, n)) 392 + ctrl = wx.ToggleButton(self, button_id, label=label)
  393 + ctrl.SetMinSize((gui_utils.calc_width_needed(ctrl, 3), -1))
  394 + ctrl.SetToolTip(wx.ToolTip(tip))
  395 + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, n, ctrl=ctrl))
  396 +
  397 + self.btns_set_fiducial[n + 3] = ctrl
802 398
803 # TODO: Find a better allignment between FRE, text and navigate button 399 # TODO: Find a better allignment between FRE, text and navigate button
804 txt_fre = wx.StaticText(self, -1, _('FRE:')) 400 txt_fre = wx.StaticText(self, -1, _('FRE:'))
805 txt_icp = wx.StaticText(self, -1, _('Refine:')) 401 txt_icp = wx.StaticText(self, -1, _('Refine:'))
806 402
807 - if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: 403 + if pedal_connection is not None and pedal_connection.in_use:
808 txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) 404 txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:'))
809 else: 405 else:
810 txt_pedal_pressed = None 406 txt_pedal_pressed = None
@@ -833,14 +429,14 @@ class NeuronavigationPanel(wx.Panel): @@ -833,14 +429,14 @@ class NeuronavigationPanel(wx.Panel):
833 self.checkbox_icp = checkbox_icp 429 self.checkbox_icp = checkbox_icp
834 430
835 # An indicator for pedal trigger 431 # An indicator for pedal trigger
836 - if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: 432 + if pedal_connection is not None and pedal_connection.in_use:
837 tooltip = wx.ToolTip(_(u"Is the pedal pressed")) 433 tooltip = wx.ToolTip(_(u"Is the pedal pressed"))
838 checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) 434 checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' '))
839 checkbox_pedal_pressed.SetValue(False) 435 checkbox_pedal_pressed.SetValue(False)
840 checkbox_pedal_pressed.Enable(False) 436 checkbox_pedal_pressed.Enable(False)
841 checkbox_pedal_pressed.SetToolTip(tooltip) 437 checkbox_pedal_pressed.SetToolTip(tooltip)
842 438
843 - self.pedal_connection.add_callback('gui', checkbox_pedal_pressed.SetValue) 439 + pedal_connection.add_callback('gui', checkbox_pedal_pressed.SetValue)
844 440
845 self.checkbox_pedal_pressed = checkbox_pedal_pressed 441 self.checkbox_pedal_pressed = checkbox_pedal_pressed
846 else: 442 else:
@@ -874,7 +470,7 @@ class NeuronavigationPanel(wx.Panel): @@ -874,7 +470,7 @@ class NeuronavigationPanel(wx.Panel):
874 (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) 470 (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
875 471
876 pedal_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) 472 pedal_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5)
877 - if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: 473 + if HAS_PEDAL_CONNECTION and pedal_connection.in_use:
878 pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), 474 pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL),
879 (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) 475 (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
880 476
@@ -899,7 +495,7 @@ class NeuronavigationPanel(wx.Panel): @@ -899,7 +495,7 @@ class NeuronavigationPanel(wx.Panel):
899 Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials') 495 Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials')
900 Publisher.subscribe(self.SetImageFiducial, 'Set image fiducial') 496 Publisher.subscribe(self.SetImageFiducial, 'Set image fiducial')
901 Publisher.subscribe(self.SetTrackerFiducial, 'Set tracker fiducial') 497 Publisher.subscribe(self.SetTrackerFiducial, 'Set tracker fiducial')
902 - Publisher.subscribe(self.UpdateTriggerState, 'Update trigger state') 498 + Publisher.subscribe(self.UpdateSerialPort, 'Update serial port')
903 Publisher.subscribe(self.UpdateTrackObjectState, 'Update track object state') 499 Publisher.subscribe(self.UpdateTrackObjectState, 'Update track object state')
904 Publisher.subscribe(self.UpdateImageCoordinates, 'Set cross focal point') 500 Publisher.subscribe(self.UpdateImageCoordinates, 'Set cross focal point')
905 Publisher.subscribe(self.OnDisconnectTracker, 'Disconnect tracker') 501 Publisher.subscribe(self.OnDisconnectTracker, 'Disconnect tracker')
@@ -920,14 +516,14 @@ class NeuronavigationPanel(wx.Panel): @@ -920,14 +516,14 @@ class NeuronavigationPanel(wx.Panel):
920 Publisher.subscribe(self.OnStartNavigation, 'Start navigation') 516 Publisher.subscribe(self.OnStartNavigation, 'Start navigation')
921 Publisher.subscribe(self.OnStopNavigation, 'Stop navigation') 517 Publisher.subscribe(self.OnStopNavigation, 'Stop navigation')
922 518
923 - def LoadImageFiducials(self, marker_id, coord):  
924 - fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'label', marker_id) 519 + def LoadImageFiducials(self, label, coord):
  520 + fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'label', label)
925 521
926 fiducial_index = fiducial['fiducial_index'] 522 fiducial_index = fiducial['fiducial_index']
927 fiducial_name = fiducial['fiducial_name'] 523 fiducial_name = fiducial['fiducial_name']
928 524
929 if self.btns_set_fiducial[fiducial_index].GetValue(): 525 if self.btns_set_fiducial[fiducial_index].GetValue():
930 - print("Fiducial {} already set, not resetting".format(marker_id)) 526 + print("Fiducial {} already set, not resetting".format(label))
931 return 527 return
932 528
933 Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=coord[0:3]) 529 Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=coord[0:3])
@@ -956,7 +552,12 @@ class NeuronavigationPanel(wx.Panel): @@ -956,7 +552,12 @@ class NeuronavigationPanel(wx.Panel):
956 fiducial = self.GetFiducialByAttribute(const.TRACKER_FIDUCIALS, 'fiducial_name', fiducial_name) 552 fiducial = self.GetFiducialByAttribute(const.TRACKER_FIDUCIALS, 'fiducial_name', fiducial_name)
957 fiducial_index = fiducial['fiducial_index'] 553 fiducial_index = fiducial['fiducial_index']
958 554
959 - self.tracker.SetTrackerFiducial(fiducial_index) 555 + # XXX: The reference mode is fetched from navigation object, however it seems like not quite
  556 + # navigation-related attribute here, as the reference mode used during the fiducial registration
  557 + # is more concerned with the calibration than the navigation.
  558 + #
  559 + ref_mode_id = self.navigation.GetReferenceMode()
  560 + self.tracker.SetTrackerFiducial(ref_mode_id, fiducial_index)
960 561
961 self.ResetICP() 562 self.ResetICP()
962 self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) 563 self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre)
@@ -986,7 +587,7 @@ class NeuronavigationPanel(wx.Panel): @@ -986,7 +587,7 @@ class NeuronavigationPanel(wx.Panel):
986 self.navigation.seed_radius = data 587 self.navigation.seed_radius = data
987 588
988 def UpdateSleep(self, data): 589 def UpdateSleep(self, data):
989 - self.navigation.sleep_nav = data 590 + self.navigation.UpdateSleep(data)
990 591
991 def UpdateNumberThreads(self, data): 592 def UpdateNumberThreads(self, data):
992 self.navigation.n_threads = data 593 self.navigation.n_threads = data
@@ -1005,7 +606,7 @@ class NeuronavigationPanel(wx.Panel): @@ -1005,7 +606,7 @@ class NeuronavigationPanel(wx.Panel):
1005 606
1006 def UpdateImageCoordinates(self, position): 607 def UpdateImageCoordinates(self, position):
1007 # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication. 608 # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication.
1008 - self.navigation.current_coord = position 609 + self.current_coord = position
1009 610
1010 for m in [0, 1, 2]: 611 for m in [0, 1, 2]:
1011 if not self.btns_set_fiducial[m].GetValue(): 612 if not self.btns_set_fiducial[m].GetValue():
@@ -1015,11 +616,11 @@ class NeuronavigationPanel(wx.Panel): @@ -1015,11 +616,11 @@ class NeuronavigationPanel(wx.Panel):
1015 def UpdateObjectRegistration(self, data=None): 616 def UpdateObjectRegistration(self, data=None):
1016 self.navigation.obj_reg = data 617 self.navigation.obj_reg = data
1017 618
1018 - def UpdateTrackObjectState(self, evt=None, flag=None, obj_name=None, polydata=None): 619 + def UpdateTrackObjectState(self, evt=None, flag=None, obj_name=None, polydata=None, use_default_object=True):
1019 self.navigation.track_obj = flag 620 self.navigation.track_obj = flag
1020 621
1021 - def UpdateTriggerState(self, trigger_state):  
1022 - self.navigation.trigger_state = trigger_state 622 + def UpdateSerialPort(self, serial_port):
  623 + self.navigation.serial_port = serial_port
1023 624
1024 def ResetICP(self): 625 def ResetICP(self):
1025 self.icp.ResetICP() 626 self.icp.ResetICP()
@@ -1032,19 +633,14 @@ class NeuronavigationPanel(wx.Panel): @@ -1032,19 +633,14 @@ class NeuronavigationPanel(wx.Panel):
1032 self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) 633 self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre)
1033 634
1034 def OnChooseTracker(self, evt, ctrl): 635 def OnChooseTracker(self, evt, ctrl):
1035 - #Publisher.sendMessage('Update status text in GUI',  
1036 - # label=_("Configuring tracker ...")) 636 + Publisher.sendMessage('Update status text in GUI',
  637 + label=_("Configuring tracker ..."))
1037 if hasattr(evt, 'GetSelection'): 638 if hasattr(evt, 'GetSelection'):
1038 choice = evt.GetSelection() 639 choice = evt.GetSelection()
1039 else: 640 else:
1040 choice = None 641 choice = None
1041 642
1042 self.tracker.SetTracker(choice) 643 self.tracker.SetTracker(choice)
1043 - #sleep(5)  
1044 - # dco.ReceiveCoordinates(self.tracker.trk_init, self.tracker.tracker_id,  
1045 - # self.tracker.TrackerCoordinates, self.navigation.event).start()  
1046 - #sleep(5)  
1047 -  
1048 if self.tracker.tracker_id == const.ROBOT: 644 if self.tracker.tracker_id == const.ROBOT:
1049 self.robot.SetRobotQueues([self.navigation.robottarget_queue, 645 self.robot.SetRobotQueues([self.navigation.robottarget_queue,
1050 self.navigation.objattarget_queue]) 646 self.navigation.objattarget_queue])
@@ -1060,7 +656,14 @@ class NeuronavigationPanel(wx.Panel): @@ -1060,7 +656,14 @@ class NeuronavigationPanel(wx.Panel):
1060 Publisher.sendMessage('Update status text in GUI', label=_("Ready")) 656 Publisher.sendMessage('Update status text in GUI', label=_("Ready"))
1061 657
1062 def OnChooseReferenceMode(self, evt, ctrl): 658 def OnChooseReferenceMode(self, evt, ctrl):
1063 - self.tracker.SetReferenceMode(evt.GetSelection()) 659 + self.navigation.SetReferenceMode(evt.GetSelection())
  660 +
  661 + # When ref mode is changed the tracker coordinates are set to zero
  662 + self.tracker.ResetTrackerFiducials()
  663 +
  664 + # Some trackers do not accept restarting within this time window
  665 + # TODO: Improve the restarting of trackers after changing reference mode
  666 +
1064 self.ResetICP() 667 self.ResetICP()
1065 668
1066 print("Reference mode changed!") 669 print("Reference mode changed!")
@@ -1069,7 +672,7 @@ class NeuronavigationPanel(wx.Panel): @@ -1069,7 +672,7 @@ class NeuronavigationPanel(wx.Panel):
1069 fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] 672 fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name']
1070 673
1071 # XXX: This is still a bit hard to read, could be cleaned up. 674 # XXX: This is still a bit hard to read, could be cleaned up.
1072 - marker_id = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0] 675 + label = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0]
1073 676
1074 if self.btns_set_fiducial[n].GetValue(): 677 if self.btns_set_fiducial[n].GetValue():
1075 coord = self.numctrls_fiducial[n][0].GetValue(),\ 678 coord = self.numctrls_fiducial[n][0].GetValue(),\
@@ -1083,17 +686,40 @@ class NeuronavigationPanel(wx.Panel): @@ -1083,17 +686,40 @@ class NeuronavigationPanel(wx.Panel):
1083 seed = 3 * [0.] 686 seed = 3 * [0.]
1084 687
1085 Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size, 688 Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size,
1086 - marker_id=marker_id, seed=seed) 689 + label=label, seed=seed)
1087 else: 690 else:
1088 for m in [0, 1, 2]: 691 for m in [0, 1, 2]:
1089 self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) 692 self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m]))
1090 693
1091 Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=np.nan) 694 Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=np.nan)
1092 - Publisher.sendMessage('Delete fiducial marker', marker_id=marker_id) 695 + Publisher.sendMessage('Delete fiducial marker', label=label)
1093 696
1094 - def OnTrackerFiducials(self, n, evt):  
1095 - fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name']  
1096 - Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) 697 + def OnTrackerFiducials(self, n, evt, ctrl):
  698 +
  699 + # Do not allow several tracker fiducials to be set at the same time.
  700 + if self.tracker_fiducial_being_set is not None and self.tracker_fiducial_being_set != n:
  701 + ctrl.SetValue(False)
  702 + return
  703 +
  704 + # Called when the button for setting the tracker fiducial is enabled and either pedal is pressed
  705 + # or the button is pressed again.
  706 + #
  707 + def set_fiducial_callback(state):
  708 + if state:
  709 + fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name']
  710 + Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name)
  711 + if self.pedal_connection is not None:
  712 + self.pedal_connection.remove_callback('fiducial')
  713 +
  714 + ctrl.SetValue(False)
  715 + self.tracker_fiducial_being_set = None
  716 +
  717 + if ctrl.GetValue():
  718 + self.tracker_fiducial_being_set = n
  719 + if self.pedal_connection is not None:
  720 + self.pedal_connection.add_callback('fiducial', set_fiducial_callback)
  721 + else:
  722 + set_fiducial_callback(True)
1097 723
1098 def OnStopNavigation(self): 724 def OnStopNavigation(self):
1099 select_tracker_elem = self.select_tracker_elem 725 select_tracker_elem = self.select_tracker_elem
@@ -1197,14 +823,16 @@ class NeuronavigationPanel(wx.Panel): @@ -1197,14 +823,16 @@ class NeuronavigationPanel(wx.Panel):
1197 # TODO: Reset camera initial focus 823 # TODO: Reset camera initial focus
1198 Publisher.sendMessage('Reset cam clipping range') 824 Publisher.sendMessage('Reset cam clipping range')
1199 self.navigation.StopNavigation() 825 self.navigation.StopNavigation()
1200 - self.navigation.__init__() 826 + self.navigation.__init__(
  827 + pedal_connection=self.pedal_connection,
  828 + )
1201 self.tracker.__init__() 829 self.tracker.__init__()
1202 self.icp.__init__() 830 self.icp.__init__()
1203 self.robot.__init__() 831 self.robot.__init__()
1204 832
1205 833
1206 class ObjectRegistrationPanel(wx.Panel): 834 class ObjectRegistrationPanel(wx.Panel):
1207 - def __init__(self, parent): 835 + def __init__(self, parent, tracker, pedal_connection):
1208 wx.Panel.__init__(self, parent) 836 wx.Panel.__init__(self, parent)
1209 try: 837 try:
1210 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) 838 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
@@ -1214,6 +842,9 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1214,6 +842,9 @@ class ObjectRegistrationPanel(wx.Panel):
1214 842
1215 self.coil_list = const.COIL 843 self.coil_list = const.COIL
1216 844
  845 + self.tracker = tracker
  846 + self.pedal_connection = pedal_connection
  847 +
1217 self.nav_prop = None 848 self.nav_prop = None
1218 self.obj_fiducials = None 849 self.obj_fiducials = None
1219 self.obj_orients = None 850 self.obj_orients = None
@@ -1323,14 +954,10 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1323,14 +954,10 @@ class ObjectRegistrationPanel(wx.Panel):
1323 self.Update() 954 self.Update()
1324 955
1325 def __bind_events(self): 956 def __bind_events(self):
1326 - Publisher.subscribe(self.UpdateTrackerInit, 'Update tracker initializer')  
1327 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') 957 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
1328 Publisher.subscribe(self.OnCloseProject, 'Close project data') 958 Publisher.subscribe(self.OnCloseProject, 'Close project data')
1329 Publisher.subscribe(self.OnRemoveObject, 'Remove object data') 959 Publisher.subscribe(self.OnRemoveObject, 'Remove object data')
1330 960
1331 - def UpdateTrackerInit(self, nav_prop):  
1332 - self.nav_prop = nav_prop  
1333 -  
1334 def UpdateNavigationStatus(self, nav_status, vis_status): 961 def UpdateNavigationStatus(self, nav_status, vis_status):
1335 if nav_status: 962 if nav_status:
1336 self.checkrecordcoords.Enable(1) 963 self.checkrecordcoords.Enable(1)
@@ -1378,11 +1005,11 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1378,11 +1005,11 @@ class ObjectRegistrationPanel(wx.Panel):
1378 1005
1379 def OnLinkCreate(self, event=None): 1006 def OnLinkCreate(self, event=None):
1380 1007
1381 - if self.nav_prop:  
1382 - dialog = dlg.ObjectCalibrationDialog(self.nav_prop) 1008 + if self.tracker.IsTrackerInitialized():
  1009 + dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection)
1383 try: 1010 try:
1384 if dialog.ShowModal() == wx.ID_OK: 1011 if dialog.ShowModal() == wx.ID_OK:
1385 - self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata = dialog.GetValue() 1012 + self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue()
1386 if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all(): 1013 if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all():
1387 self.checktrack.Enable(1) 1014 self.checktrack.Enable(1)
1388 Publisher.sendMessage('Update object registration', 1015 Publisher.sendMessage('Update object registration',
@@ -1391,7 +1018,13 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1391,7 +1018,13 @@ class ObjectRegistrationPanel(wx.Panel):
1391 label=_("Ready")) 1018 label=_("Ready"))
1392 # Enable automatically Track object, Show coil and disable Vol. Camera 1019 # Enable automatically Track object, Show coil and disable Vol. Camera
1393 self.checktrack.SetValue(True) 1020 self.checktrack.SetValue(True)
1394 - Publisher.sendMessage('Update track object state', flag=True, obj_name=self.obj_name, polydata=polydata) 1021 + Publisher.sendMessage(
  1022 + 'Update track object state',
  1023 + flag=True,
  1024 + obj_name=self.obj_name,
  1025 + polydata=polydata,
  1026 + use_default_object=use_default_object,
  1027 + )
1395 Publisher.sendMessage('Change camera checkbox', status=False) 1028 Publisher.sendMessage('Change camera checkbox', status=False)
1396 1029
1397 except wx._core.PyAssertionError: # TODO FIX: win64 1030 except wx._core.PyAssertionError: # TODO FIX: win64
@@ -1468,6 +1101,76 @@ class ObjectRegistrationPanel(wx.Panel): @@ -1468,6 +1101,76 @@ class ObjectRegistrationPanel(wx.Panel):
1468 1101
1469 1102
1470 class MarkersPanel(wx.Panel): 1103 class MarkersPanel(wx.Panel):
  1104 + @dataclasses.dataclass
  1105 + class Marker:
  1106 + """Class for storing markers. @dataclass decorator simplifies
  1107 + setting default values, serialization, etc."""
  1108 + x : float = 0
  1109 + y : float = 0
  1110 + z : float = 0
  1111 + alpha : float = 0
  1112 + beta : float = 0
  1113 + gamma : float = 0
  1114 + r : float = 0
  1115 + g : float = 1
  1116 + b : float = 0
  1117 + size : int = 2
  1118 + label : str = '*'
  1119 + x_seed : float = 0
  1120 + y_seed : float = 0
  1121 + z_seed : float = 0
  1122 + is_target : int = 0 # is_target is int instead of boolean to avoid
  1123 + # problems with CSV export
  1124 +
  1125 + # x, y, z, alpha, beta, gamma can be jointly accessed as coord
  1126 + @property
  1127 + def coord(self):
  1128 + return list((self.x, self.y, self.z, self.alpha, self.beta, self.gamma),)
  1129 +
  1130 + @coord.setter
  1131 + def coord(self, new_coord):
  1132 + self.x, self.y, self.z, self.alpha, self.beta, self.gamma = new_coord
  1133 +
  1134 + # r, g, b can be jointly accessed as colour
  1135 + @property
  1136 + def colour(self):
  1137 + return list((self.r, self.g, self.b),)
  1138 +
  1139 + @colour.setter
  1140 + def colour(self, new_colour):
  1141 + self.r, self.g, self.b = new_colour
  1142 +
  1143 + # x_seed, y_seed, z_seed can be jointly accessed as seed
  1144 + @property
  1145 + def seed(self):
  1146 + return list((self.x_seed, self.y_seed, self.z_seed),)
  1147 +
  1148 + @seed.setter
  1149 + def seed(self, new_seed):
  1150 + self.x_seed, self.y_seed, self.z_seed = new_seed
  1151 +
  1152 + @classmethod
  1153 + def get_headers(cls):
  1154 + """Return the list of field names (headers) for exporting to csv."""
  1155 + res = [field.name for field in dataclasses.fields(cls)]
  1156 + res.extend(['x_world', 'y_world', 'z_world', 'alpha_world', 'beta_world', 'gamma_world'])
  1157 + return res
  1158 +
  1159 + def get_values(self):
  1160 + """Return the list of values for exporting to csv."""
  1161 + res = []
  1162 + res.extend(dataclasses.astuple(self))
  1163 +
  1164 + # Add world coordinates (in addition to the internal ones).
  1165 + position_world, orientation_world = imagedata_utils.convert_invesalius_to_world(
  1166 + position=[self.x, self.y, self.z],
  1167 + orientation=[self.alpha, self.beta, self.gamma],
  1168 + )
  1169 + res.extend(position_world)
  1170 + res.extend(orientation_world)
  1171 +
  1172 + return res
  1173 +
1471 def __init__(self, parent): 1174 def __init__(self, parent):
1472 wx.Panel.__init__(self, parent) 1175 wx.Panel.__init__(self, parent)
1473 try: 1176 try:
@@ -1483,6 +1186,7 @@ class MarkersPanel(wx.Panel): @@ -1483,6 +1186,7 @@ class MarkersPanel(wx.Panel):
1483 self.current_coord = 0, 0, 0, 0, 0, 0 1186 self.current_coord = 0, 0, 0, 0, 0, 0
1484 self.current_angle = 0, 0, 0 1187 self.current_angle = 0, 0, 0
1485 self.current_seed = 0, 0, 0 1188 self.current_seed = 0, 0, 0
  1189 + self.markers = []
1486 self.current_ref = 0, 0, 0, 0, 0, 0 1190 self.current_ref = 0, 0, 0, 0, 0, 0
1487 self.current_robot = 0, 0, 0, 0, 0, 0 1191 self.current_robot = 0, 0, 0, 0, 0, 0
1488 self.list_coord = [] 1192 self.list_coord = []
@@ -1492,12 +1196,12 @@ class MarkersPanel(wx.Panel): @@ -1492,12 +1196,12 @@ class MarkersPanel(wx.Panel):
1492 self.mchange = None 1196 self.mchange = None
1493 self.flag_target = False 1197 self.flag_target = False
1494 1198
1495 - # self.timer = wx.Timer(self)  
1496 - # self.Bind(wx.EVT_TIMER, self.OnUpdateSendCoord, self.timer)  
1497 -  
1498 self.marker_colour = const.MARKER_COLOUR 1199 self.marker_colour = const.MARKER_COLOUR
1499 self.marker_size = const.MARKER_SIZE 1200 self.marker_size = const.MARKER_SIZE
1500 1201
  1202 + # Define CSV dialect for saving/loading markers
  1203 + csv.register_dialect('markers_dialect', delimiter='\t', quoting=csv.QUOTE_NONNUMERIC)
  1204 +
1501 # Change marker size 1205 # Change marker size
1502 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23)) 1206 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
1503 spin_size.SetRange(1, 99) 1207 spin_size.SetRange(1, 99)
@@ -1534,7 +1238,7 @@ class MarkersPanel(wx.Panel): @@ -1534,7 +1238,7 @@ class MarkersPanel(wx.Panel):
1534 1238
1535 # Buttons to delete or remove markers 1239 # Buttons to delete or remove markers
1536 btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23)) 1240 btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23))
1537 - btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteSingleMarker) 1241 + btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteMultipleMarkers)
1538 1242
1539 btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23)) 1243 btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23))
1540 btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers) 1244 btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers)
@@ -1550,11 +1254,15 @@ class MarkersPanel(wx.Panel): @@ -1550,11 +1254,15 @@ class MarkersPanel(wx.Panel):
1550 self.lc.InsertColumn(2, 'Y') 1254 self.lc.InsertColumn(2, 'Y')
1551 self.lc.InsertColumn(3, 'Z') 1255 self.lc.InsertColumn(3, 'Z')
1552 self.lc.InsertColumn(4, 'ID') 1256 self.lc.InsertColumn(4, 'ID')
  1257 + self.lc.InsertColumn(5, 'Target')
  1258 +
1553 self.lc.SetColumnWidth(0, 28) 1259 self.lc.SetColumnWidth(0, 28)
1554 self.lc.SetColumnWidth(1, 50) 1260 self.lc.SetColumnWidth(1, 50)
1555 self.lc.SetColumnWidth(2, 50) 1261 self.lc.SetColumnWidth(2, 50)
1556 self.lc.SetColumnWidth(3, 50) 1262 self.lc.SetColumnWidth(3, 50)
1557 self.lc.SetColumnWidth(4, 60) 1263 self.lc.SetColumnWidth(4, 60)
  1264 + self.lc.SetColumnWidth(5, 60)
  1265 +
1558 self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown) 1266 self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown)
1559 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink) 1267 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink)
1560 self.lc.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnStopItemBlink) 1268 self.lc.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnStopItemBlink)
@@ -1573,7 +1281,7 @@ class MarkersPanel(wx.Panel): @@ -1573,7 +1281,7 @@ class MarkersPanel(wx.Panel):
1573 def __bind_events(self): 1281 def __bind_events(self):
1574 # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points') 1282 # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points')
1575 Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point') 1283 Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point')
1576 - Publisher.subscribe(self.OnDeleteSingleMarker, 'Delete fiducial marker') 1284 + Publisher.subscribe(self.OnDeleteMultipleMarkers, 'Delete fiducial marker')
1577 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') 1285 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers')
1578 Publisher.subscribe(self.CreateMarker, 'Create marker') 1286 Publisher.subscribe(self.CreateMarker, 'Create marker')
1579 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') 1287 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
@@ -1584,6 +1292,70 @@ class MarkersPanel(wx.Panel): @@ -1584,6 +1292,70 @@ class MarkersPanel(wx.Panel):
1584 Publisher.subscribe(self.UpdateObjectMarker2Center, 'Update object marker to center') 1292 Publisher.subscribe(self.UpdateObjectMarker2Center, 'Update object marker to center')
1585 Publisher.subscribe(self.OnObjectTarget, 'Coil at target') 1293 Publisher.subscribe(self.OnObjectTarget, 'Coil at target')
1586 1294
  1295 + def __find_target_marker(self):
  1296 + """Return the index of the marker currently selected as target (there
  1297 + should be at most one). If there is no such marker, return -1."""
  1298 + for i in range(len(self.markers)):
  1299 + if self.markers[i].is_target:
  1300 + return i
  1301 +
  1302 + return -1
  1303 +
  1304 + def __get_selected_items(self):
  1305 + """
  1306 + Returns a (possibly empty) list of the selected items in the list control.
  1307 + """
  1308 + selection = []
  1309 +
  1310 + next = self.lc.GetFirstSelected()
  1311 +
  1312 + while next != -1:
  1313 + selection.append(next)
  1314 + next = self.lc.GetNextSelected(next)
  1315 +
  1316 + return selection
  1317 +
  1318 + def __delete_multiple_markers(self, index):
  1319 + """ Delete multiple markers indexed by index. index must be sorted in
  1320 + the ascending order.
  1321 + """
  1322 + for i in reversed(index):
  1323 + del self.markers[i]
  1324 + self.lc.DeleteItem(i)
  1325 + for n in range(0, self.lc.GetItemCount()):
  1326 + self.lc.SetItem(n, 0, str(n+1))
  1327 + Publisher.sendMessage('Remove multiple markers', index=index)
  1328 +
  1329 + def __set_marker_as_target(self, idx):
  1330 + """Set marker indexed by idx as the new target. idx must be a valid index."""
  1331 + # Find the previous target
  1332 + prev_idx = self.__find_target_marker()
  1333 +
  1334 + # If the new target is same as the previous do nothing.
  1335 + if prev_idx == idx:
  1336 + return
  1337 +
  1338 + # Unset the previous target
  1339 + if prev_idx != -1:
  1340 + self.markers[prev_idx].is_target = 0
  1341 + self.lc.SetItemBackgroundColour(prev_idx, 'white')
  1342 + Publisher.sendMessage('Set target transparency', status=False, index=prev_idx)
  1343 + self.lc.SetItem(prev_idx, 5, "")
  1344 +
  1345 + # Set the new target
  1346 + self.markers[idx].is_target = 1
  1347 + self.lc.SetItemBackgroundColour(idx, 'RED')
  1348 + self.lc.SetItem(idx, 5, _("Yes"))
  1349 +
  1350 + Publisher.sendMessage('Update target', coord=self.markers[idx].coord)
  1351 + Publisher.sendMessage('Set target transparency', status=True, index=idx)
  1352 + wx.MessageBox(_("New target selected."), _("InVesalius 3"))
  1353 +
  1354 + @staticmethod
  1355 + def __list_fiducial_labels():
  1356 + """Return the list of marker labels denoting fucials."""
  1357 + return list(itertools.chain(*(const.BTNS_IMG_MARKERS[i].values() for i in const.BTNS_IMG_MARKERS)))
  1358 +
1587 def UpdateCurrentCoord(self, position): 1359 def UpdateCurrentCoord(self, position):
1588 self.current_coord = position 1360 self.current_coord = position
1589 #self.current_angle = pubsub_evt.data[1][3:] 1361 #self.current_angle = pubsub_evt.data[1][3:]
@@ -1620,7 +1392,7 @@ class MarkersPanel(wx.Panel): @@ -1620,7 +1392,7 @@ class MarkersPanel(wx.Panel):
1620 # TODO: Enable the "Set as target" only when target is created with registered object 1392 # TODO: Enable the "Set as target" only when target is created with registered object
1621 menu_id = wx.Menu() 1393 menu_id = wx.Menu()
1622 edit_id = menu_id.Append(0, _('Edit ID')) 1394 edit_id = menu_id.Append(0, _('Edit ID'))
1623 - menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerId, edit_id) 1395 + menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id)
1624 color_id = menu_id.Append(2, _('Edit color')) 1396 color_id = menu_id.Append(2, _('Edit color'))
1625 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id) 1397 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id)
1626 menu_id.AppendSeparator() 1398 menu_id.AppendSeparator()
@@ -1644,54 +1416,29 @@ class MarkersPanel(wx.Panel): @@ -1644,54 +1416,29 @@ class MarkersPanel(wx.Panel):
1644 def OnStopItemBlink(self, evt): 1416 def OnStopItemBlink(self, evt):
1645 Publisher.sendMessage('Stop Blink Marker') 1417 Publisher.sendMessage('Stop Blink Marker')
1646 1418
1647 - def OnMenuEditMarkerId(self, evt): 1419 + def OnMenuEditMarkerLabel(self, evt):
1648 list_index = self.lc.GetFocusedItem() 1420 list_index = self.lc.GetFocusedItem()
1649 - if evt == 'TARGET':  
1650 - id_label = evt 1421 + if list_index != -1:
  1422 + new_label = dlg.ShowEnterMarkerID(self.lc.GetItemText(list_index, 4))
  1423 + self.markers[list_index].label = str(new_label)
  1424 + self.lc.SetItem(list_index, 4, new_label)
1651 else: 1425 else:
1652 - id_label = dlg.ShowEnterMarkerID(self.lc.GetItemText(list_index, 4))  
1653 - if id_label == 'TARGET':  
1654 - id_label = '*'  
1655 - wx.MessageBox(_("Invalid TARGET ID."), _("InVesalius 3"))  
1656 -  
1657 - # Add the new ID to exported list  
1658 - if len(self.list_coord[list_index]) > 8:  
1659 - self.list_coord[list_index][10] = str(id_label)  
1660 - else:  
1661 - self.list_coord[list_index][7] = str(id_label)  
1662 -  
1663 - self.lc.SetItem(list_index, 4, id_label) 1426 + wx.MessageBox(_("No data selected."), _("InVesalius 3"))
1664 1427
1665 def OnMenuSetTarget(self, evt): 1428 def OnMenuSetTarget(self, evt):
1666 - if isinstance(evt, int):  
1667 - self.lc.Focus(evt)  
1668 -  
1669 - if self.tgt_flag:  
1670 - marker_id = '*'  
1671 -  
1672 - self.lc.SetItemBackgroundColour(self.tgt_index, 'white')  
1673 - Publisher.sendMessage('Set target transparency', status=False, index=self.tgt_index)  
1674 - self.lc.SetItem(self.tgt_index, 4, marker_id)  
1675 -  
1676 - # Add the new ID to exported list  
1677 - if len(self.list_coord[self.tgt_index]) > 8:  
1678 - self.list_coord[self.tgt_index][10] = marker_id  
1679 - else:  
1680 - self.list_coord[self.tgt_index][7] = marker_id  
1681 -  
1682 - self.tgt_index = self.lc.GetFocusedItem()  
1683 - self.lc.SetItemBackgroundColour(self.tgt_index, 'RED')  
1684 -  
1685 - Publisher.sendMessage('Update target', coord=self.list_coord[self.tgt_index][:6])  
1686 - Publisher.sendMessage('Set target transparency', status=True, index=self.tgt_index)  
1687 - self.OnMenuEditMarkerId('TARGET')  
1688 - self.tgt_flag = True  
1689 - wx.MessageBox(_("New target selected."), _("InVesalius 3")) 1429 + idx = self.lc.GetFocusedItem()
  1430 + if idx != -1:
  1431 + self.__set_marker_as_target(idx)
  1432 + else:
  1433 + wx.MessageBox(_("No data selected."), _("InVesalius 3"))
1690 1434
1691 def OnMenuSetColor(self, evt): 1435 def OnMenuSetColor(self, evt):
1692 index = self.lc.GetFocusedItem() 1436 index = self.lc.GetFocusedItem()
  1437 + if index == -1:
  1438 + wx.MessageBox(_("No data selected."), _("InVesalius 3"))
  1439 + return
1693 1440
1694 - color_current = [self.list_coord[index][n] * 255 for n in range(6, 9)] 1441 + color_current = [ch * 255 for ch in self.markers[index].colour]
1695 1442
1696 color_new = dlg.ShowColorDialog(color_current=color_current) 1443 color_new = dlg.ShowColorDialog(color_current=color_current)
1697 1444
@@ -1701,7 +1448,7 @@ class MarkersPanel(wx.Panel): @@ -1701,7 +1448,7 @@ class MarkersPanel(wx.Panel):
1701 # XXX: Seems like a slightly too early point for rounding; better to round only when the value 1448 # XXX: Seems like a slightly too early point for rounding; better to round only when the value
1702 # is printed to the screen or file. 1449 # is printed to the screen or file.
1703 # 1450 #
1704 - self.list_coord[index][6:9] = [round(s/255.0, 3) for s in color_new] 1451 + self.markers[index].colour = [round(s/255.0, 3) for s in color_new]
1705 1452
1706 Publisher.sendMessage('Set new color', index=index, color=color_new) 1453 Publisher.sendMessage('Set new color', index=index, color=color_new)
1707 1454
@@ -1738,151 +1485,95 @@ class MarkersPanel(wx.Panel): @@ -1738,151 +1485,95 @@ class MarkersPanel(wx.Panel):
1738 1485
1739 1486
1740 def OnDeleteAllMarkers(self, evt=None): 1487 def OnDeleteAllMarkers(self, evt=None):
1741 - if self.list_coord:  
1742 - if evt is None:  
1743 - result = wx.ID_OK  
1744 - else:  
1745 - # result = dlg.DeleteAllMarkers()  
1746 - result = dlg.ShowConfirmationDialog(msg=_("Remove all markers? Cannot be undone."))  
1747 -  
1748 - if result == wx.ID_OK:  
1749 - self.list_coord = []  
1750 - self.marker_ind = 0  
1751 - Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount())  
1752 - self.lc.DeleteAllItems()  
1753 - Publisher.sendMessage('Stop Blink Marker', index='DeleteAll')  
1754 -  
1755 - if self.tgt_flag:  
1756 - self.tgt_flag = self.tgt_index = None  
1757 - Publisher.sendMessage('Disable or enable coil tracker', status=False)  
1758 - if not hasattr(evt, 'data'):  
1759 - wx.MessageBox(_("Target deleted."), _("InVesalius 3"))  
1760 -  
1761 - def OnDeleteSingleMarker(self, evt=None, marker_id=None):  
1762 - # OnDeleteSingleMarker is used for both pubsub and button click events 1488 + if evt is None:
  1489 + result = wx.ID_OK
  1490 + else:
  1491 + result = dlg.ShowConfirmationDialog(msg=_("Remove all markers? Cannot be undone."))
  1492 +
  1493 + if result != wx.ID_OK:
  1494 + return
  1495 +
  1496 + if self.__find_target_marker() != -1:
  1497 + Publisher.sendMessage('Disable or enable coil tracker', status=False)
  1498 + if evt is not None:
  1499 + wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
  1500 +
  1501 + self.markers = []
  1502 + Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount())
  1503 + self.lc.DeleteAllItems()
  1504 + Publisher.sendMessage('Stop Blink Marker', index='DeleteAll')
  1505 +
  1506 + def OnDeleteMultipleMarkers(self, evt=None, label=None):
  1507 + # OnDeleteMultipleMarkers is used for both pubsub and button click events
1763 # Pubsub is used for fiducial handle and button click for all others 1508 # Pubsub is used for fiducial handle and button click for all others
1764 1509
1765 - if not evt:  
1766 - if self.lc.GetItemCount(): 1510 + if not evt: # called through pubsub
  1511 + index = []
  1512 +
  1513 + if label and (label in self.__list_fiducial_labels()):
1767 for id_n in range(self.lc.GetItemCount()): 1514 for id_n in range(self.lc.GetItemCount()):
1768 item = self.lc.GetItem(id_n, 4) 1515 item = self.lc.GetItem(id_n, 4)
1769 - if item.GetText() == marker_id:  
1770 - for i in const.BTNS_IMG_MARKERS:  
1771 - if marker_id in list(const.BTNS_IMG_MARKERS[i].values())[0]:  
1772 - self.lc.Focus(item.GetId())  
1773 - index = [self.lc.GetFocusedItem()]  
1774 - else:  
1775 - if self.lc.GetFirstSelected() != -1:  
1776 - index = self.GetSelectedItems()  
1777 - else:  
1778 - index = None 1516 + if item.GetText() == label:
  1517 + self.lc.Focus(item.GetId())
  1518 + index = [self.lc.GetFocusedItem()]
  1519 +
  1520 + else: # called from button click
  1521 + index = self.__get_selected_items()
1779 1522
1780 - #TODO: There are bugs when no marker is selected, test and improve  
1781 if index: 1523 if index:
1782 - if self.tgt_flag and self.tgt_index == index[0]:  
1783 - self.tgt_flag = self.tgt_index = None 1524 + if self.__find_target_marker() in index:
1784 Publisher.sendMessage('Disable or enable coil tracker', status=False) 1525 Publisher.sendMessage('Disable or enable coil tracker', status=False)
  1526 + wx.MessageBox(_("Target deleted."), _("InVesalius 3"))
  1527 +
  1528 + self.__delete_multiple_markers(index)
  1529 + else:
  1530 + if evt: # Don't show the warning if called through pubsub
1785 #TODO: reset robot target. (target should be the same target as invesalius?) 1531 #TODO: reset robot target. (target should be the same target as invesalius?)
1786 Publisher.sendMessage('Robot target matrix', robot_tracker_flag=False, 1532 Publisher.sendMessage('Robot target matrix', robot_tracker_flag=False,
1787 m_change_robot2ref=None) 1533 m_change_robot2ref=None)
1788 wx.MessageBox(_("No data selected."), _("InVesalius 3")) 1534 wx.MessageBox(_("No data selected."), _("InVesalius 3"))
1789 1535
1790 - self.DeleteMarker(index)  
1791 - else:  
1792 - wx.MessageBox(_("Target deleted."), _("InVesalius 3"))  
1793 -  
1794 - def DeleteMarker(self, index):  
1795 - for i in reversed(index):  
1796 - del self.list_coord[i]  
1797 - self.lc.DeleteItem(i)  
1798 - for n in range(0, self.lc.GetItemCount()):  
1799 - self.lc.SetItem(n, 0, str(n+1))  
1800 - self.marker_ind -= 1  
1801 - Publisher.sendMessage('Remove marker', index=index)  
1802 -  
1803 - def OnCreateMarker(self, evt=None, coord=None, marker_id=None, colour=None): 1536 + def OnCreateMarker(self, evt):
1804 self.CreateMarker() 1537 self.CreateMarker()
1805 1538
1806 def OnLoadMarkers(self, evt): 1539 def OnLoadMarkers(self, evt):
  1540 + """Loads markers from file and appends them to the current marker list.
  1541 + The file should contain no more than a single target marker. Also the
  1542 + file should not contain any fiducials already in the list."""
1807 filename = dlg.ShowLoadSaveDialog(message=_(u"Load markers"), 1543 filename = dlg.ShowLoadSaveDialog(message=_(u"Load markers"),
1808 wildcard=const.WILDCARD_MARKER_FILES) 1544 wildcard=const.WILDCARD_MARKER_FILES)
1809 - # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609'  
1810 - # marker_path = 'markers.mks'  
1811 - # filename = os.path.join(data_dir, marker_path)  
1812 1545
1813 - if filename:  
1814 - try:  
1815 - count_line = self.lc.GetItemCount()  
1816 - # content = [s.rstrip() for s in open(filename)]  
1817 - with open(filename, 'r') as file:  
1818 - reader = csv.reader(file, delimiter='\t')  
1819 -  
1820 - # skip the header  
1821 - if filename.lower().endswith('.mkss'):  
1822 - next(reader)  
1823 -  
1824 - content = [row for row in reader]  
1825 -  
1826 - for line in content:  
1827 - target = None  
1828 - if len(line) > 8:  
1829 - coord = [float(s) for s in line[:6]]  
1830 - colour = [float(s) for s in line[6:9]]  
1831 - size = float(line[9])  
1832 - marker_id = line[10]  
1833 -  
1834 - if len(line) > 11:  
1835 - seed = [float(s) for s in line[11:14]]  
1836 - else:  
1837 - seed = 3 * [0.]  
1838 - if len(line) > 12:  
1839 - robot = [float(s) for s in line[14:20]]  
1840 - ref = [float(s) for s in line[20:26]]  
1841 - else:  
1842 - robot = 0., 0., 0., 0., 0., 0.  
1843 - ref = 0., 0., 0., 0., 0., 0.  
1844 -  
1845 - if len(line) >= 11:  
1846 - for i in const.BTNS_IMG_MARKERS:  
1847 - if marker_id in list(const.BTNS_IMG_MARKERS[i].values())[0]:  
1848 - Publisher.sendMessage('Load image fiducials', marker_id=marker_id, coord=coord)  
1849 - elif marker_id == 'TARGET':  
1850 - target = count_line  
1851 - else:  
1852 - marker_id = '*'  
1853 -  
1854 - if len(line) == 15:  
1855 - target_id = line[14]  
1856 - else:  
1857 - target_id = '*'  
1858 - else:  
1859 - # for compatibility with previous version without the extra seed and target columns  
1860 - coord = float(line[0]), float(line[1]), float(line[2]), 0, 0, 0  
1861 - colour = float(line[3]), float(line[4]), float(line[5])  
1862 - size = float(line[6])  
1863 -  
1864 - seed = 3 * [0]  
1865 - target_id = '*'  
1866 -  
1867 - if len(line) == 8:  
1868 - marker_id = line[7]  
1869 - for i in const.BTNS_IMG_MARKERS:  
1870 - if marker_id in list(const.BTNS_IMG_MARKERS[i].values())[0]:  
1871 - Publisher.sendMessage('Load image fiducials', marker_id=marker_id, coord=coord)  
1872 - else:  
1873 - marker_id = '*'  
1874 -  
1875 - self.CreateMarker(coord=coord, colour=colour, size=size,  
1876 - marker_id=marker_id, target_id=target_id, seed=seed,  
1877 - robot=self.current_robot, ref=self.current_ref)  
1878 -  
1879 - # if there are multiple TARGETS will set the last one  
1880 - if target:  
1881 - self.OnMenuSetTarget(target)  
1882 -  
1883 - count_line += 1  
1884 - except:  
1885 - wx.MessageBox(_("Invalid markers file."), _("InVesalius 3")) 1546 + if not filename:
  1547 + return
  1548 +
  1549 + try:
  1550 + with open(filename, 'r') as file:
  1551 + magick_line = file.readline()
  1552 + assert magick_line.startswith(const.MARKER_FILE_MAGICK_STRING)
  1553 + ver = int(magick_line.split('_')[-1])
  1554 + if ver != 0:
  1555 + wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3"))
  1556 + return
  1557 +
  1558 + reader = csv.reader(file, dialect='markers_dialect')
  1559 + next(reader) # skip the header line
  1560 +
  1561 + # Read the data lines and create markers
  1562 + for line in reader:
  1563 + marker = self.Marker(*line[:-6]) # Discard the last 6 fields (the world coordinates)
  1564 + self.CreateMarker(coord=marker.coord, colour=marker.colour, size=marker.size,
  1565 + label=marker.label, is_target=0, seed=marker.seed)
  1566 +
  1567 + if marker.label in self.__list_fiducial_labels():
  1568 + Publisher.sendMessage('Load image fiducials', label=marker.label, coord=marker.coord)
  1569 +
  1570 + # If the new marker has is_target=1 (True), we first create
  1571 + # a marker with is_target=0 (False), and then call __set_marker_as_target
  1572 + if marker.is_target:
  1573 + self.__set_marker_as_target(len(self.markers)-1)
  1574 +
  1575 + except:
  1576 + wx.MessageBox(_("Invalid markers file."), _("InVesalius 3"))
1886 1577
1887 def OnMarkersVisibility(self, evt, ctrl): 1578 def OnMarkersVisibility(self, evt, ctrl):
1888 1579
@@ -1905,79 +1596,55 @@ class MarkersPanel(wx.Panel): @@ -1905,79 +1596,55 @@ class MarkersPanel(wx.Panel):
1905 filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."), 1596 filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."),
1906 wildcard=const.WILDCARD_MARKER_FILES, 1597 wildcard=const.WILDCARD_MARKER_FILES,
1907 style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, 1598 style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
1908 - default_filename="markers.mks", save_ext="mks")  
1909 -  
1910 - header_titles = ['x', 'y', 'z', 'alpha', 'beta', 'gamma', 'r', 'g', 'b',  
1911 - 'size', 'marker_id', 'x_seed', 'y_seed', 'z_seed', 'target_id'] 1599 + default_filename=default_filename)
1912 1600
1913 - if filename:  
1914 - if self.list_coord:  
1915 - with open(filename, 'w', newline='') as file:  
1916 - writer = csv.writer(file, delimiter='\t')  
1917 -  
1918 - if filename.lower().endswith('.mkss'):  
1919 - writer.writerow(header_titles) 1601 + if not filename:
  1602 + return
1920 1603
1921 - writer.writerows(self.list_coord) 1604 + try:
  1605 + with open(filename, 'w', newline='') as file:
  1606 + file.writelines(['%s%i\n' % (const.MARKER_FILE_MAGICK_STRING, const.CURRENT_MARKER_FILE_VERSION)])
  1607 + writer = csv.writer(file, dialect='markers_dialect')
  1608 + writer.writerow(self.Marker.get_headers())
  1609 + writer.writerows(marker.get_values() for marker in self.markers)
  1610 + file.close()
  1611 + except:
  1612 + wx.MessageBox(_("Error writing markers file."), _("InVesalius 3"))
1922 1613
1923 def OnSelectColour(self, evt, ctrl): 1614 def OnSelectColour(self, evt, ctrl):
1924 - self.marker_colour = [colour/255.0 for colour in ctrl.GetValue()] 1615 + #TODO: Make sure GetValue returns 3 numbers (without alpha)
  1616 + self.marker_colour = [colour/255.0 for colour in ctrl.GetValue()][:3]
1925 1617
1926 def OnSelectSize(self, evt, ctrl): 1618 def OnSelectSize(self, evt, ctrl):
1927 self.marker_size = ctrl.GetValue() 1619 self.marker_size = ctrl.GetValue()
1928 1620
1929 - def CreateMarker(self, coord=None, colour=None, size=None, marker_id='*', target_id='*', seed=None, robot=None, ref=None):  
1930 - coord = coord or self.current_coord  
1931 - colour = colour or self.marker_colour  
1932 - size = size or self.marker_size  
1933 - seed = seed or self.current_seed  
1934 - robot = robot or self.current_robot  
1935 - ref = ref or self.current_ref  
1936 - # TODO: Use matrix coordinates and not world coordinates as current method.  
1937 - # This makes easier for inter-software comprehension.  
1938 -  
1939 - Publisher.sendMessage('Add marker', ball_id=self.marker_ind, size=size, colour=colour, coord=coord[0:3])  
1940 -  
1941 - self.marker_ind += 1  
1942 -  
1943 - # List of lists with coordinates and properties of a marker  
1944 - line = []  
1945 - line.extend(coord)  
1946 - line.extend(colour)  
1947 - line.append(size)  
1948 - line.append(marker_id)  
1949 - line.extend(seed)  
1950 - line.extend(robot)  
1951 - line.extend(ref)  
1952 - line.append(target_id)  
1953 -  
1954 - # Adding current line to a list of all markers already created  
1955 - if not self.list_coord:  
1956 - self.list_coord = [line]  
1957 - else:  
1958 - self.list_coord.append(line) 1621 + def CreateMarker(self, coord=None, colour=None, size=None, label='*', is_target=0, seed=None, robot=None, ref=None):
  1622 + new_marker = self.Marker()
  1623 + new_marker.coord = coord or self.current_coord
  1624 + new_marker.colour = colour or self.marker_colour
  1625 + new_marker.size = size or self.marker_size
  1626 + new_marker.label = label
  1627 + new_marker.is_target = is_target
  1628 + new_marker.seed = seed or self.current_seed
  1629 + new_marker.robot = robot or self.current_robot
  1630 + new_marker.ref = ref or self.current_ref
  1631 +
  1632 + # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added
  1633 + Publisher.sendMessage('Add marker', ball_id=len(self.markers),
  1634 + size=new_marker.size,
  1635 + colour=new_marker.colour,
  1636 + coord=new_marker.coord[:3])
  1637 + self.markers.append(new_marker)
1959 1638
1960 # Add item to list control in panel 1639 # Add item to list control in panel
1961 num_items = self.lc.GetItemCount() 1640 num_items = self.lc.GetItemCount()
1962 self.lc.InsertItem(num_items, str(num_items + 1)) 1641 self.lc.InsertItem(num_items, str(num_items + 1))
1963 - self.lc.SetItem(num_items, 1, str(round(coord[0], 2)))  
1964 - self.lc.SetItem(num_items, 2, str(round(coord[1], 2)))  
1965 - self.lc.SetItem(num_items, 3, str(round(coord[2], 2)))  
1966 - self.lc.SetItem(num_items, 4, str(marker_id)) 1642 + self.lc.SetItem(num_items, 1, str(round(new_marker.x, 2)))
  1643 + self.lc.SetItem(num_items, 2, str(round(new_marker.y, 2)))
  1644 + self.lc.SetItem(num_items, 3, str(round(new_marker.z, 2)))
  1645 + self.lc.SetItem(num_items, 4, str(new_marker.label))
1967 self.lc.EnsureVisible(num_items) 1646 self.lc.EnsureVisible(num_items)
1968 1647
1969 - def GetSelectedItems(self):  
1970 - """  
1971 - Returns a list of the selected items in the list control.  
1972 - """  
1973 - selection = []  
1974 - index = self.lc.GetFirstSelected()  
1975 - selection.append(index)  
1976 - while len(selection) != self.lc.GetSelectedItemCount():  
1977 - index = self.lc.GetNextSelected(index)  
1978 - selection.append(index)  
1979 - return selection  
1980 -  
1981 class DbsPanel(wx.Panel): 1648 class DbsPanel(wx.Panel):
1982 def __init__(self, parent): 1649 def __init__(self, parent):
1983 wx.Panel.__init__(self, parent) 1650 wx.Panel.__init__(self, parent)
@@ -2429,101 +2096,6 @@ class TractographyPanel(wx.Panel): @@ -2429,101 +2096,6 @@ class TractographyPanel(wx.Panel):
2429 Publisher.sendMessage('Remove tracts') 2096 Publisher.sendMessage('Remove tracts')
2430 2097
2431 2098
2432 -class QueueCustom(queue.Queue):  
2433 - """  
2434 - A custom queue subclass that provides a :meth:`clear` method.  
2435 - https://stackoverflow.com/questions/6517953/clear-all-items-from-the-queue  
2436 - Modified to a LIFO Queue type (Last-in-first-out). Seems to make sense for the navigation  
2437 - threads, as the last added coordinate should be the first to be processed.  
2438 - In the first tests in a short run, seems to increase the coord queue size considerably,  
2439 - possibly limiting the queue size is good.  
2440 - """  
2441 -  
2442 - def clear(self):  
2443 - """  
2444 - Clears all items from the queue.  
2445 - """  
2446 -  
2447 - with self.mutex:  
2448 - unfinished = self.unfinished_tasks - len(self.queue)  
2449 - if unfinished <= 0:  
2450 - if unfinished < 0:  
2451 - raise ValueError('task_done() called too many times')  
2452 - self.all_tasks_done.notify_all()  
2453 - self.unfinished_tasks = unfinished  
2454 - self.queue.clear()  
2455 - self.not_full.notify_all()  
2456 -  
2457 -  
2458 -class UpdateNavigationScene(threading.Thread):  
2459 -  
2460 - def __init__(self, vis_queues, vis_components, event, sle):  
2461 - """Class (threading) to update the navigation scene with all graphical elements.  
2462 -  
2463 - Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation  
2464 -  
2465 - :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene  
2466 - :type affine_vtk: vtkMatrix4x4  
2467 - :param visualization_queue: Queue instance that manage coordinates to be visualized  
2468 - :type visualization_queue: queue.Queue  
2469 - :param event: Threading event to coordinate when tasks as done and allow UI release  
2470 - :type event: threading.Event  
2471 - :param sle: Sleep pause in seconds  
2472 - :type sle: float  
2473 - """  
2474 -  
2475 - threading.Thread.__init__(self, name='UpdateScene')  
2476 - self.trigger_state, self.view_tracts, self.peel_loaded = vis_components  
2477 - self.coord_queue, self.trigger_queue, self.tracts_queue, self.icp_queue, self.robottarget_queue = vis_queues  
2478 - self.sle = sle  
2479 - self.event = event  
2480 -  
2481 - def run(self):  
2482 - # count = 0  
2483 - while not self.event.is_set():  
2484 - got_coords = False  
2485 - try:  
2486 - coord, [coord_raw, markers_flag], m_img, view_obj = self.coord_queue.get_nowait()  
2487 - got_coords = True  
2488 -  
2489 - # print('UpdateScene: get {}'.format(count))  
2490 -  
2491 - # use of CallAfter is mandatory otherwise crashes the wx interface  
2492 - if self.view_tracts:  
2493 - bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait()  
2494 - #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command  
2495 - wx.CallAfter(Publisher.sendMessage, 'Remove tracts')  
2496 - wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle,  
2497 - affine_vtk=affine_vtk, coord_offset=coord_offset)  
2498 - # wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset)  
2499 - self.tracts_queue.task_done()  
2500 -  
2501 - if self.trigger_state:  
2502 - trigger_on = self.trigger_queue.get_nowait()  
2503 - if trigger_on:  
2504 - wx.CallAfter(Publisher.sendMessage, 'Create marker')  
2505 - self.trigger_queue.task_done()  
2506 -  
2507 - #TODO: If using the view_tracts substitute the raw coord from the offset coordinate, so the user  
2508 - # see the red cross in the position of the offset marker  
2509 - wx.CallAfter(Publisher.sendMessage, 'Update slices position', position=coord[:3])  
2510 - wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord)  
2511 - wx.CallAfter(Publisher.sendMessage, 'Update raw coord', coord_raw=coord_raw, markers_flag=markers_flag)  
2512 - wx.CallAfter(Publisher.sendMessage, 'Update slice viewer')  
2513 -  
2514 - if view_obj:  
2515 - wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)  
2516 - wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix',m_img=m_img, coord=coord, flag= self.peel_loaded)  
2517 - self.coord_queue.task_done()  
2518 - # print('UpdateScene: done {}'.format(count))  
2519 - # count += 1  
2520 -  
2521 - sleep(self.sle)  
2522 - except queue.Empty:  
2523 - if got_coords:  
2524 - self.coord_queue.task_done()  
2525 -  
2526 -  
2527 class InputAttributes(object): 2099 class InputAttributes(object):
2528 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python 2100 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
2529 def __init__(self, *initial_data, **kwargs): 2101 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, 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, markers_flag = tracker.TrackerCoordinates.GetCoordinates()
  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,158 @@ @@ -0,0 +1,158 @@
  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 +import threading
  22 +
  23 +import invesalius.constants as const
  24 +import invesalius.data.coordinates as dco
  25 +import invesalius.data.trackers as dt
  26 +import invesalius.gui.dialogs as dlg
  27 +from invesalius.pubsub import pub as Publisher
  28 +
  29 +
  30 +class Tracker():
  31 + def __init__(self):
  32 + self.trk_init = None
  33 + self.tracker_id = const.DEFAULT_TRACKER
  34 +
  35 + self.tracker_fiducials = np.full([3, 3], np.nan)
  36 + self.tracker_fiducials_raw = np.zeros((6, 6))
  37 +
  38 + self.tracker_connected = False
  39 +
  40 + self.thread_coord = None
  41 +
  42 + self.event_coord = threading.Event()
  43 +
  44 + self.TrackerCoordinates = dco.TrackerCoordinates()
  45 +
  46 + def SetTracker(self, new_tracker):
  47 + if new_tracker:
  48 + self.DisconnectTracker()
  49 +
  50 + self.trk_init = dt.TrackerConnection(new_tracker, None, 'connect')
  51 + if not self.trk_init[0]:
  52 + dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1])
  53 +
  54 + self.tracker_id = 0
  55 + self.tracker_connected = False
  56 + else:
  57 + self.tracker_id = new_tracker
  58 + self.tracker_connected = True
  59 + self.thread_coord = dco.ReceiveCoordinates(self.trk_init, self.tracker_id, self.TrackerCoordinates,
  60 + self.event_coord)
  61 + self.thread_coord.start()
  62 +
  63 + def DisconnectTracker(self):
  64 + if self.tracker_connected:
  65 + self.ResetTrackerFiducials()
  66 + Publisher.sendMessage('Update status text in GUI',
  67 + label=_("Disconnecting tracker ..."))
  68 + Publisher.sendMessage('Remove sensors ID')
  69 + Publisher.sendMessage('Remove object data')
  70 + self.trk_init = dt.TrackerConnection(self.tracker_id, self.trk_init[0], 'disconnect')
  71 + if not self.trk_init[0]:
  72 + self.tracker_connected = False
  73 + self.tracker_id = 0
  74 +
  75 + if self.thread_coord:
  76 + self.event_coord.set()
  77 + self.thread_coord.join()
  78 + self.event_coord.clear()
  79 +
  80 + Publisher.sendMessage('Update status text in GUI',
  81 + label=_("Tracker disconnected"))
  82 + print("Tracker disconnected!")
  83 + else:
  84 + Publisher.sendMessage('Update status text in GUI',
  85 + label=_("Tracker still connected"))
  86 + print("Tracker still connected!")
  87 +
  88 + def IsTrackerInitialized(self):
  89 + return self.trk_init and self.tracker_id and self.tracker_connected
  90 +
  91 + def AreTrackerFiducialsSet(self):
  92 + return not np.isnan(self.tracker_fiducials).any()
  93 +
  94 + def GetTrackerCoordinates(self, ref_mode_id, n_samples=1):
  95 + coord_raw_samples = {}
  96 + coord_samples = {}
  97 +
  98 + for i in range(n_samples):
  99 + coord_raw, markers_flag = self.TrackerCoordinates.GetCoordinates()
  100 +
  101 + if ref_mode_id == const.DYNAMIC_REF:
  102 + coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :])
  103 + else:
  104 + coord = coord_raw[0, :]
  105 + coord[2] = -coord[2]
  106 +
  107 + coord_raw_samples[i] = coord_raw
  108 + coord_samples[i] = coord
  109 +
  110 + coord_raw_avg = np.median(list(coord_raw_samples.values()), axis=0)
  111 + coord_avg = np.median(list(coord_samples.values()), axis=0)
  112 +
  113 + return coord_avg, coord_raw_avg
  114 +
  115 + def SetTrackerFiducial(self, ref_mode_id, fiducial_index):
  116 + coord, coord_raw = self.GetTrackerCoordinates(
  117 + ref_mode_id=ref_mode_id,
  118 + n_samples=const.CALIBRATION_TRACKER_SAMPLES,
  119 + )
  120 +
  121 + # Update tracker fiducial with tracker coordinates
  122 + self.tracker_fiducials[fiducial_index, :] = coord[0:3]
  123 +
  124 + assert 0 <= fiducial_index <= 2, "Fiducial index out of range (0-2): {}".format(fiducial_index)
  125 +
  126 + self.tracker_fiducials_raw[2 * fiducial_index, :] = coord_raw[0, :]
  127 + self.tracker_fiducials_raw[2 * fiducial_index + 1, :] = coord_raw[1, :]
  128 +
  129 + print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3]))
  130 +
  131 + def ResetTrackerFiducials(self):
  132 + for m in range(3):
  133 + self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan]
  134 +
  135 + def GetTrackerFiducials(self):
  136 + return self.tracker_fiducials, self.tracker_fiducials_raw
  137 +
  138 + def GetTrackerInfo(self):
  139 + return self.trk_init, self.tracker_id
  140 +
  141 + def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre):
  142 + if self.tracker_connected:
  143 + selection_ctrl.SetSelection(self.tracker_id)
  144 + else:
  145 + selection_ctrl.SetSelection(0)
  146 +
  147 + # Update tracker location in the UI.
  148 + for m in range(3):
  149 + coord = self.tracker_fiducials[m, :]
  150 + for n in range(0, 3):
  151 + value = 0.0 if np.isnan(coord[n]) else float(coord[n])
  152 + numctrls_fiducial[m][n].SetValue(value)
  153 +
  154 + txtctrl_fre.SetValue('')
  155 + txtctrl_fre.SetBackgroundColour('WHITE')
  156 +
  157 + def get_trackers(self):
  158 + return const.TRACKERS
invesalius/net/pedal_connection.py
@@ -22,6 +22,7 @@ from threading import Thread @@ -22,6 +22,7 @@ from threading import Thread
22 22
23 import mido 23 import mido
24 24
  25 +from invesalius.pubsub import pub as Publisher
25 from invesalius.utils import Singleton 26 from invesalius.utils import Singleton
26 27
27 class PedalConnection(Thread, metaclass=Singleton): 28 class PedalConnection(Thread, metaclass=Singleton):
@@ -50,6 +51,8 @@ class PedalConnection(Thread, metaclass=Singleton): @@ -50,6 +51,8 @@ class PedalConnection(Thread, metaclass=Singleton):
50 if not self._callbacks: 51 if not self._callbacks:
51 print("Pedal pressed, no callbacks registered") 52 print("Pedal pressed, no callbacks registered")
52 else: 53 else:
  54 + Publisher.sendMessage('Pedal state changed', state=True)
  55 +
53 for callback in self._callbacks.values(): 56 for callback in self._callbacks.values():
54 callback(True) 57 callback(True)
55 58
@@ -57,9 +60,10 @@ class PedalConnection(Thread, metaclass=Singleton): @@ -57,9 +60,10 @@ class PedalConnection(Thread, metaclass=Singleton):
57 if not self._callbacks: 60 if not self._callbacks:
58 print("Pedal released, no callbacks registered") 61 print("Pedal released, no callbacks registered")
59 else: 62 else:
  63 + Publisher.sendMessage('Pedal state changed', state=False)
  64 +
60 for callback in self._callbacks.values(): 65 for callback in self._callbacks.values():
61 callback(False) 66 callback(False)
62 -  
63 else: 67 else:
64 print("Unknown message type received from MIDI device") 68 print("Unknown message type received from MIDI device")
65 69
@@ -70,6 +74,8 @@ class PedalConnection(Thread, metaclass=Singleton): @@ -70,6 +74,8 @@ class PedalConnection(Thread, metaclass=Singleton):
70 self._midi_in._rt.ignore_types(False, False, False) 74 self._midi_in._rt.ignore_types(False, False, False)
71 self._midi_in.callback = self._midi_to_pedal 75 self._midi_in.callback = self._midi_to_pedal
72 76
  77 + Publisher.sendMessage('Pedal connection', state=True)
  78 +
73 print("Connected to MIDI device") 79 print("Connected to MIDI device")
74 80
75 def _check_disconnected(self): 81 def _check_disconnected(self):
@@ -77,6 +83,8 @@ class PedalConnection(Thread, metaclass=Singleton): @@ -77,6 +83,8 @@ class PedalConnection(Thread, metaclass=Singleton):
77 if self._active_input not in self._midi_inputs: 83 if self._active_input not in self._midi_inputs:
78 self._midi_in = None 84 self._midi_in = None
79 85
  86 + Publisher.sendMessage('Pedal connection', state=False)
  87 +
80 print("Disconnected from MIDI device") 88 print("Disconnected from MIDI device")
81 89
82 def _update_midi_inputs(self): 90 def _update_midi_inputs(self):
navigation/objects/5ch_Simplified_scaled.stl 0 → 100644
No preview for this file type
requirements.txt
1 -Cython==0.29.22  
2 -Pillow==8.2.0 1 +Cython==0.29.24
  2 +Pillow==8.3.2
3 Pypubsub==4.0.3 3 Pypubsub==4.0.3
4 configparser==5.0.1 4 configparser==5.0.1
5 h5py==2.10.0 5 h5py==2.10.0
6 imageio==2.9.0 6 imageio==2.9.0
7 nibabel==3.2.1 7 nibabel==3.2.1
8 -numpy==1.20.1 8 +numpy==1.21.2
9 plaidml-keras==0.7.0 9 plaidml-keras==0.7.0
10 psutil==5.8.0 10 psutil==5.8.0
11 pyserial==3.5 11 pyserial==3.5
12 -python-gdcm==3.0.8.1  
13 -scikit-image==0.18.1  
14 -scipy==1.6.1  
15 -vtk==9.0.1  
16 -wxPython==4.1.0 12 +python-gdcm==3.0.9.1
  13 +scikit-image==0.18.3
  14 +scipy==1.7.1
  15 +vtk==9.0.3
  16 +wxPython==4.1.1
17 Theano==1.0.5 17 Theano==1.0.5