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
.gitignore
1   -invesalius/*.classpath
2   -invesalius/*.hg
3   -invesalius/*.log
4   -invesalius/*.project
5   -invesalius/*.pyc
6   -invesalius/*.swn
7   -invesalius/*.swo
8   -invesalius/*.swp
9   -invesalius/data/*.log
10   -invesalius/data/*.pyc
11   -invesalius/data/*.pyd
12   -invesalius/gui/*.log
13   -invesalius/gui/*.pyc
14   -invesalius/gui/widgets/*.log
15   -invesalius/gui/widgets/*.pyc
16   -invesalius/reader/*.log
17   -invesalius/reader/*.pyc
18   -
19   -.idea
20   -
21   -*.pyc
22   -*.swp
  1 +# Created by https://www.toptal.com/developers/gitignore/api/vim,python,intellij,visualstudiocode,direnv
  2 +# Edit at https://www.toptal.com/developers/gitignore?templates=vim,python,intellij,visualstudiocode,direnv
  3 +
  4 +### direnv ###
  5 +.direnv
  6 +.envrc
  7 +
  8 +### Intellij ###
  9 +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
  10 +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
  11 +.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 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 210 *.aux
  211 +*.lof
  212 +*.log
  213 +*.lot
  214 +*.fls
  215 +*.out
41 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 236 *.bbl
  237 +*.bcf
43 238 *.blg
44   -*.fls
  239 +*-blx.aux
  240 +*-blx.bib
  241 +*.run.xml
  242 +
  243 +## Build tool auxiliary files:
45 244 *.fdb_latexmk
  245 +*.synctex
  246 +*.synctex(busy)
46 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
... ...
app.py
... ... @@ -209,9 +209,9 @@ class Inv3SplashScreen(SplashScreen):
209 209  
210 210 else:
211 211  
212   - path = os.path.join(".","icons", icon_file)
  212 + path = os.path.join(inv_paths.ICON_DIR, icon_file)
213 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 216 bmp = wx.Image(path).ConvertToBitmap()
217 217  
... ...
environment.yml
... ... @@ -3,25 +3,23 @@ channels:
3 3 - bioconda
4 4 dependencies:
5 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 8 - pypubsub==4.0.3
10 9 - configparser==5.0.1
11 10 - h5py==2.10.0
12 11 - imageio==2.9.0
13 12 - nibabel==3.2.1
14   - - numpy==1.20.1
15   - - pooch==1.4.0
  13 + - numpy==1.21.2
16 14 - psutil==5.8.0
17 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 20 - pip
23 21 - pip:
24   - - python-gdcm==3.0.8.1
  22 + - python-gdcm==3.0.9.1
25 23 - plaidml-keras==0.7.0
26 24 - theano==1.0.5
27 25 - pyacvd==0.2.3
... ...
invesalius/constants.py
... ... @@ -659,6 +659,8 @@ BOOLEAN_XOR = 4
659 659  
660 660 MARKER_COLOUR = (1.0, 1.0, 0.)
661 661 MARKER_SIZE = 2
  662 +
  663 +CALIBRATION_TRACKER_SAMPLES = 10
662 664 FIDUCIAL_REGISTRATION_ERROR_THRESHOLD = 3.0
663 665  
664 666 SELECT = 0
... ... @@ -760,21 +762,42 @@ OBJA = wx.NewId()
760 762 OBJC = wx.NewId()
761 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 798 MTC_PROBE_NAME = "1Probe"
776 799 MTC_REF_NAME = "2Ref"
777   -MTC_OBJ_NAME = "3bigcoil"
  800 +MTC_OBJ_NAME = "3Coil"
778 801  
779 802 # Object tracking
780 803 ARROW_SCALE = 6
... ... @@ -805,8 +828,9 @@ TREKKER_CONFIG = {'seed_max': 1, 'step_size': 0.1, 'min_fod': 0.1, 'probe_qualit
805 828 'write_interval': 50, 'numb_threads': '', 'max_lenth': 200,
806 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 835 ROBOT_ElFIN_IP = ['Select robot IP:', '143.107.220.251', '169.254.153.251', '127.0.0.1']
812 836 ROBOT_ElFIN_PORT = 10003
... ...
invesalius/control.py
... ... @@ -915,6 +915,16 @@ class Controller():
915 915 matrix, matrix_filename = self.OpenOtherFiles(group)
916 916 self.CreateOtherProject(name, matrix, matrix_filename)
917 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 928 Publisher.sendMessage("Enable state project", state=True)
919 929 else:
920 930 dialog.ImportInvalidFiles(ftype="Others")
... ... @@ -1043,17 +1053,6 @@ class Controller():
1043 1053 self.Slice.window_level = wl
1044 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 1056 scalar_range = int(scalar_range[0]), int(scalar_range[1])
1058 1057 Publisher.sendMessage('Update threshold limits list',
1059 1058 threshold_range=scalar_range)
... ...
invesalius/data/bases.py
... ... @@ -188,10 +188,18 @@ def object_registration(fiducials, orients, coord_raw, m_change):
188 188 fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3]
189 189  
190 190 # compute initial alignment of probe fixed in the object in source frame
191   - 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 204 # compute change of basis for object fiducials in source frame
197 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 218 fids_dyn[ic, 2] = -fids_dyn[ic, 2]
211 219  
212 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 226 M_img = m_change @ M_p
218 227  
219 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 237 r_obj_img[:3, :3] = base_obj_img[:3, :3]
229 238  
230 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 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 149 # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html
150 150 def gdcm_to_numpy(image, apply_intercep_scale=True):
151 151 map_gdcm_np = {
  152 + gdcm.PixelFormat.SINGLEBIT: np.uint8,
152 153 gdcm.PixelFormat.UINT8: np.uint8,
153 154 gdcm.PixelFormat.INT8: np.int8,
154 155 gdcm.PixelFormat.UINT12: np.uint16,
... ... @@ -177,6 +178,8 @@ def gdcm_to_numpy(image, apply_intercep_scale=True):
177 178 np_array = np.frombuffer(
178 179 gdcm_array.encode("utf-8", errors="surrogateescape"), dtype=dtype
179 180 )
  181 + if pf.GetScalarType() == gdcm.PixelFormat.SINGLEBIT:
  182 + np_array = np.unpackbits(np_array)
180 183 np_array.shape = shape
181 184 np_array = np_array.squeeze()
182 185  
... ...
invesalius/data/coordinates.py
... ... @@ -39,9 +39,9 @@ class TrackerCoordinates():
39 39 def SetCoordinates(self, coord, markers_flag):
40 40 self.coord = coord
41 41 self.markers_flag = markers_flag
  42 + wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)
42 43  
43 44 def GetCoordinates(self):
44   - wx.CallAfter(Publisher.sendMessage, 'Sensors ID', markers_flag=self.markers_flag)
45 45 return self.coord, self.markers_flag
46 46  
47 47  
... ... @@ -179,7 +179,7 @@ def PolarisCoord(trck_init, trck_id, ref_mode):
179 179  
180 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 184 def ElfinCoord(trck_init):
185 185 if len(trck_init) > 2:
... ... @@ -384,11 +384,45 @@ def DebugCoordRandom(trk_init, trck_id, ref_mode):
384 384 # coord4 = np.array([uniform(1, 200), uniform(1, 200), uniform(1, 200),
385 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 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 426 def dynamic_reference(probe, reference):
393 427 """
394 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 469 :param reference: sensor two defined as reference
436 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 477 probe_4 = np.vstack((probe[:3].reshape([3, 1]), 1.))
444 478 coord_rot = np.linalg.inv(affine) @ probe_4
445 479 # minus sign to the z coordinate
... ... @@ -479,15 +513,16 @@ def dynamic_reference_m2(probe, reference):
479 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 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 69 :return: 4 x 4 numpy double array
70 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 77 m_dyn = np.linalg.inv(m_ref) @ m_probe
79 78 return m_dyn
80 79  
... ... @@ -108,20 +107,25 @@ def corregistrate_object_dynamic(inp, coord_raw, ref_mode_id, icp):
108 107  
109 108 # transform raw marker coordinate to object center
110 109 m_probe = object_marker_to_center(coord_raw, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw)
  110 +
111 111 # transform object center to reference marker if specified as dynamic reference
112 112 if ref_mode_id:
113 113 m_probe_ref = object_to_reference(coord_raw, m_probe)
114 114 else:
115 115 m_probe_ref = m_probe
  116 +
116 117 # invert y coordinate
117 118 m_probe_ref[2, -1] = -m_probe_ref[2, -1]
  119 +
118 120 # corregistrate from tracker to image space
119 121 m_img = tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn)
120 122 if icp[0]:
121 123 m_img = bases.transform_icp(m_img, icp[1])
  124 +
122 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 129 coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \
126 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 136 self.icp = flag
133 137  
134 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 144 return m_probe
140 145  
141 146  
... ... @@ -145,6 +150,7 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp):
145 150  
146 151 # transform raw marker coordinate to object center
147 152 m_probe = compute_marker_transformation(coord_raw, obj_ref_mode)
  153 +
148 154 # transform object center to reference marker if specified as dynamic reference
149 155 if ref_mode_id:
150 156 m_ref = compute_marker_transformation(coord_raw, 1)
... ... @@ -154,6 +160,7 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp):
154 160  
155 161 # invert y coordinate
156 162 m_probe_ref[2, -1] = -m_probe_ref[2, -1]
  163 +
157 164 # corregistrate from tracker to image space
158 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 168 m_img = bases.transform_icp(m_img, icp[1])
162 169  
163 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 174 coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\
167 175 np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2])
168 176  
... ... @@ -204,7 +212,7 @@ class CoordinateCorregistrate(threading.Thread):
204 212 coreg_data = self.coreg_data
205 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 217 # print('CoordCoreg: event {}'.format(self.event.is_set()))
210 218 while not self.event.is_set():
... ... @@ -284,7 +292,7 @@ class CoordinateCorregistrateNoObject(threading.Thread):
284 292 coreg_data = self.coreg_data
285 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 296 # print('CoordCoreg: event {}'.format(self.event.is_set()))
289 297 while not self.event.is_set():
290 298 try:
... ...
invesalius/data/imagedata_utils.py
... ... @@ -34,6 +34,7 @@ from vtk.util import numpy_support
34 34  
35 35 import invesalius.constants as const
36 36 import invesalius.data.converters as converters
  37 +import invesalius.data.coordinates as dco
37 38 import invesalius.data.slice_ as sl
38 39 import invesalius.data.transformations as tr
39 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 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 564 def convert_world_to_voxel(xyz, affine):
559 565 """
560 566 Convert a coordinate from the world space ((x, y, z); scanner space; millimeters) to the
561 567 voxel space ((i, j, k)). This is achieved by multiplying a coordinate by the inverse
562 568 of the affine transformation.
  569 +
563 570 More information: https://nipy.org/nibabel/coordinate_systems.html
  571 +
564 572 :param xyz: a list or array of 3 coordinates (x, y, z) in the world coordinates
565 573 :param affine: a 4x4 array containing the image affine transformation in homogeneous coordinates
566 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 576 # convert xyz coordinate to 1x4 homogeneous coordinates array
571 577 xyz_homo = np.hstack((xyz, 1.0)).reshape([4, 1])
572 578 ijk_homo = np.linalg.inv(affine) @ xyz_homo
... ... @@ -575,6 +581,65 @@ def convert_world_to_voxel(xyz, affine):
575 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 643 def create_grid(xy_range, z_range, z_offset, spacing):
579 644 x = np.arange(xy_range[0], xy_range[1] + 1, spacing)
580 645 y = np.arange(xy_range[0], xy_range[1] + 1, spacing)
... ...
invesalius/data/serial_port_connection.py 0 → 100644
... ... @@ -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 2600 class FFillSegmentationConfig(metaclass=utils.Singleton):
2601 2601 def __init__(self):
2602 2602 self.dlg_visible = False
  2603 + self.dlg = None
2603 2604 self.target = "2D"
2604 2605 self.con_2d = 4
2605 2606 self.con_3d = 6
... ... @@ -2634,7 +2635,6 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2634 2635 self.slice_data = viewer.slice_data
2635 2636  
2636 2637 self.config = FFillSegmentationConfig()
2637   - self.dlg_ffill = None
2638 2638  
2639 2639 self._progr_title = _(u"Region growing")
2640 2640 self._progr_msg = _(u"Segmenting ...")
... ... @@ -2652,14 +2652,14 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2652 2652 self.config.t1 = int(_max)
2653 2653  
2654 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 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 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 2664 def OnFFClick(self, obj, evt):
2665 2665 if (self.viewer.slice_.buffer_slices[self.orientation].mask is None):
... ... @@ -2787,22 +2787,28 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2787 2787 with futures.ThreadPoolExecutor(max_workers=1) as executor:
2788 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 2792 while not future.done():
2792   - dlg.Pulse()
  2793 + self.config.dlg.panel_ffill_progress.Pulse()
  2794 + self.config.dlg.Update()
2793 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 2798 out_mask = future.result()
2796 2799 else:
2797 2800 out_mask = np.zeros_like(mask)
2798 2801 with futures.ThreadPoolExecutor(max_workers=1) as executor:
2799 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 2806 while not future.done():
2803   - dlg.Pulse()
  2807 + self.config.dlg.panel_ffill_progress.Pulse()
  2808 + self.config.dlg.Update()
2804 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 2813 mask[out_mask.astype('bool')] = self.config.fill_value
2808 2814  
... ...
invesalius/data/trigger.py
... ... @@ -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 36 from imageio import imsave
37 37  
38 38 import invesalius.constants as const
  39 +import invesalius.data.coordinates as dco
39 40 import invesalius.data.slice_ as sl
40 41 import invesalius.data.styles_3d as styles
41 42 import invesalius.data.transformations as tr
... ... @@ -192,6 +193,7 @@ class Viewer(wx.Panel):
192 193 self.dummy_coil_actor = None
193 194 self.target_mode = False
194 195 self.polydata = None
  196 + self.use_default_object = True
195 197 self.anglethreshold = const.COIL_ANGLES_THRESHOLD
196 198 self.distthreshold = const.COIL_COORD_THRESHOLD
197 199 self.angle_arrow_projection_threshold = const.COIL_ANGLE_ARROW_PROJECTION_THRESHOLD
... ... @@ -278,7 +280,7 @@ class Viewer(wx.Panel):
278 280 Publisher.subscribe(self.HideAllMarkers, 'Hide all markers')
279 281 Publisher.subscribe(self.ShowAllMarkers, 'Show all markers')
280 282 Publisher.subscribe(self.RemoveAllMarkers, 'Remove all markers')
281   - Publisher.subscribe(self.RemoveMarker, 'Remove marker')
  283 + Publisher.subscribe(self.RemoveMultipleMarkers, 'Remove multiple markers')
282 284 Publisher.subscribe(self.BlinkMarker, 'Blink Marker')
283 285 Publisher.subscribe(self.StopBlinkMarker, 'Stop Blink Marker')
284 286 Publisher.subscribe(self.SetNewColor, 'Set new color')
... ... @@ -343,18 +345,23 @@ class Viewer(wx.Panel):
343 345 if style == const.SLICE_STATE_CROSS:
344 346 self._mode_cross = True
345 347 # self._check_and_set_ball_visibility()
  348 + #if not self.actor_peel:
346 349 self._ball_ref_visibility = True
  350 + #else:
  351 + # self._ball_ref_visibility = False
347 352 # if self._to_show_ball:
348   - if not self.ball_actor:
  353 + if not self.ball_actor: #and not self.actor_peel:
349 354 self.CreateBallReference()
350   -
  355 + #self.ball_actor.SetVisibility(1)
  356 + #else:
  357 + # self.ball_actor.SetVisibility(0)
351 358 self.interactor.Render()
352 359  
353 360 def _uncheck_ball_reference(self, style):
354 361 if style == const.SLICE_STATE_CROSS:
355 362 self._mode_cross = False
356 363 # self.RemoveBallReference()
357   - self._ball_ref_visibility = False
  364 + self._ball_ref_visibility = True
358 365 if self.ball_actor:
359 366 self.ren.RemoveActor(self.ball_actor)
360 367 self.ball_actor = None
... ... @@ -671,7 +678,7 @@ class Viewer(wx.Panel):
671 678 self.staticballs = []
672 679 self.UpdateRender()
673 680  
674   - def RemoveMarker(self, index):
  681 + def RemoveMultipleMarkers(self, index):
675 682 for i in reversed(index):
676 683 self.ren.RemoveActor(self.staticballs[i])
677 684 del self.staticballs[i]
... ... @@ -702,7 +709,7 @@ class Viewer(wx.Panel):
702 709 self.index = False
703 710  
704 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 713 self.Refresh()
707 714  
708 715 def OnTargetMarkerTransparency(self, status, index):
... ... @@ -980,10 +987,12 @@ class Viewer(wx.Panel):
980 987  
981 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 997 m_img_vtk = vtk.vtkMatrix4x4()
989 998  
... ... @@ -1019,10 +1028,10 @@ class Viewer(wx.Panel):
1019 1028 self.aim_actor = aim_actor
1020 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 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 1036 transform = vtk.vtkTransform()
1028 1037 transform.RotateZ(90)
... ... @@ -1238,6 +1247,7 @@ class Viewer(wx.Panel):
1238 1247 # self.UpdateCameraBallPosition(None, position)
1239 1248  
1240 1249 def UpdateCameraBallPosition(self, position):
  1250 + #if not self.actor_peel:
1241 1251 coord_flip = list(position[:3])
1242 1252 coord_flip[1] = -coord_flip[1]
1243 1253 self.ball_actor.SetPosition(coord_flip)
... ... @@ -1328,6 +1338,9 @@ class Viewer(wx.Panel):
1328 1338 self.ren.AddActor(self.x_actor)
1329 1339 self.ren.AddActor(self.y_actor)
1330 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 1344 #self.ren.AddActor(self.obj_projection_arrow_actor)
1332 1345 #self.ren.AddActor(self.object_orientation_torus_actor)
1333 1346 # self.obj_axes = vtk.vtkAxesActor()
... ... @@ -1442,6 +1455,8 @@ class Viewer(wx.Panel):
1442 1455 self.ren.RemoveActor(self.object_orientation_torus_actor)
1443 1456 self.ren.RemoveActor(self.obj_projection_arrow_actor)
1444 1457 self.actor_peel = None
  1458 + self.ball_actor.SetVisibility(1)
  1459 +
1445 1460 if flag and actor:
1446 1461 self.ren.AddActor(actor)
1447 1462 self.actor_peel = actor
... ... @@ -1499,7 +1514,7 @@ class Viewer(wx.Panel):
1499 1514  
1500 1515 self.ren.AddActor(self.obj_projection_arrow_actor)
1501 1516 self.ren.AddActor(self.object_orientation_torus_actor)
1502   -
  1517 + self.ball_actor.SetVisibility(0)
1503 1518 self.obj_projection_arrow_actor.SetPosition(closestPoint)
1504 1519 self.obj_projection_arrow_actor.SetOrientation(coil_dir)
1505 1520  
... ... @@ -1520,6 +1535,8 @@ class Viewer(wx.Panel):
1520 1535 self.ren.RemoveActor(self.obj_projection_arrow_actor)
1521 1536 self.ren.RemoveActor(self.object_orientation_torus_actor)
1522 1537 self.ren.RemoveActor(self.x_actor)
  1538 + self.ball_actor.SetVisibility(1)
  1539 +
1523 1540 #self.ren.RemoveActor(self.y_actor)
1524 1541 self.Refresh()
1525 1542  
... ... @@ -1531,9 +1548,9 @@ class Viewer(wx.Panel):
1531 1548 self.pTarget = self.CenterOfMass()
1532 1549 if self.obj_actor:
1533 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 1554 #self.object_orientation_torus_actor.SetVisibility(self.obj_state)
1538 1555 #self.obj_projection_arrow_actor.SetVisibility(self.obj_state)
1539 1556 self.Refresh()
... ... @@ -1599,10 +1616,11 @@ class Viewer(wx.Panel):
1599 1616 self.GetCellIntersection(p1, norm, coil_norm, coil_dir)
1600 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 1620 if flag:
1604 1621 self.obj_name = obj_name
1605 1622 self.polydata = polydata
  1623 + self.use_default_object = use_default_object
1606 1624 if not self.obj_actor:
1607 1625 self.AddObjectActor(self.obj_name)
1608 1626 else:
... ... @@ -1630,7 +1648,10 @@ class Viewer(wx.Panel):
1630 1648 self.x_actor.SetVisibility(self.obj_state)
1631 1649 self.y_actor.SetVisibility(self.obj_state)
1632 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 1655 self.Refresh()
1635 1656  
1636 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 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 2601 class FFillOptionsDialog(wx.Dialog):
2567 2602 def __init__(self, title, config):
2568 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 2855 self.panel_ffill_confidence.SetMinSize((250, -1))
2821 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 2862 self.close_btn = wx.Button(self, wx.ID_CLOSE)
2824 2863  
2825 2864 # Sizer
... ... @@ -2876,11 +2915,16 @@ class FFillSegmentationOptionsDialog(wx.Dialog):
2876 2915 sizer.Add(0, 0, (12, 0))
2877 2916 except TypeError:
2878 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 2919 try:
2881 2920 sizer.Add(0, 0, (14, 0))
2882 2921 except TypeError:
2883 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 2929 self.SetSizer(sizer)
2886 2930 sizer.Fit(self)
... ... @@ -3263,14 +3307,17 @@ class MaskDensityDialog(wx.Dialog):
3263 3307  
3264 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 3316 self.obj_ref_id = 2
3272 3317 self.obj_name = None
3273 3318 self.polydata = None
  3319 + self.use_default_object = False
  3320 + self.object_fiducial_being_set = None
3274 3321  
3275 3322 self.obj_fiducials = np.full([5, 3], np.nan)
3276 3323 self.obj_orients = np.full([5, 3], np.nan)
... ... @@ -3281,6 +3328,11 @@ class ObjectCalibrationDialog(wx.Dialog):
3281 3328 self._init_gui()
3282 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 3336 def _init_gui(self):
3285 3337 self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize())
3286 3338 self.interactor.Enable(1)
... ... @@ -3298,7 +3350,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3298 3350 choice_ref = wx.ComboBox(self, -1, "", size=wx.Size(90, 23),
3299 3351 choices=const.REF_MODE, style=wx.CB_DROPDOWN | wx.CB_READONLY)
3300 3352 choice_ref.SetToolTip(tooltip)
3301   - choice_ref.Bind(wx.EVT_COMBOBOX, self.OnChoiceRefMode)
  3353 + choice_ref.Bind(wx.EVT_COMBOBOX, self.OnChooseReferenceMode)
3302 3354 choice_ref.SetSelection(1)
3303 3355 choice_ref.Enable(1)
3304 3356 if self.tracker_id == const.PATRIOT or self.tracker_id == const.ISOTRAKII:
... ... @@ -3331,15 +3383,17 @@ class ObjectCalibrationDialog(wx.Dialog):
3331 3383 choice_sensor])
3332 3384  
3333 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 3398 for m in range(0, 5):
3345 3399 for n in range(0, 3):
... ... @@ -3383,8 +3437,9 @@ class ObjectCalibrationDialog(wx.Dialog):
3383 3437 return 0
3384 3438  
3385 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 3443 filename = ShowImportMeshFilesDialog()
3389 3444  
3390 3445 if filename:
... ... @@ -3397,11 +3452,17 @@ class ObjectCalibrationDialog(wx.Dialog):
3397 3452 elif filename.lower().endswith('.vtp'):
3398 3453 reader = vtk.vtkXMLPolyDataReader()
3399 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 3456 return
3402 3457 else:
3403 3458 filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl")
3404 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 3466 else:
3406 3467 filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl")
3407 3468 reader = vtk.vtkSTLReader()
... ... @@ -3476,39 +3537,87 @@ class ObjectCalibrationDialog(wx.Dialog):
3476 3537 self.ren.AddActor(ball_actor)
3477 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 3598 else:
3491   - coord = coord_raw[0, :]
  3599 + coord = coord_raw[self.obj_ref_id, :]
3492 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 3605 coord = np.zeros([6,])
3497 3606  
3498 3607 # Update text controls with tracker coordinates
3499 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 3616 self.Refresh()
3508 3617 else:
3509 3618 ShowNavigationTrackerWarning(0, 'choose')
3510 3619  
3511   - def OnChoiceRefMode(self, evt):
  3620 + def OnChooseReferenceMode(self, evt):
3512 3621 # When ref mode is changed the tracker coordinates are set to nan
3513 3622 # This is for Polhemus FASTRAK wrapper, where the sensor attached to the object can be the stylus (Static
3514 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 3625 # I use the index 2 directly here to send to the coregistration module where it is possible to access without
3517 3626 # any conditional statement the correct index of coordinates.
3518 3627  
3519   - if evt.GetSelection():
  3628 + if evt.GetSelection() == 1:
3520 3629 self.obj_ref_id = 2
3521 3630 if self.tracker_id in [const.FASTRAK, const.DEBUGTRACKRANDOM, const.DEBUGTRACKAPPROACH]:
3522 3631 self.choice_sensor.Show(self.obj_ref_id)
3523 3632 else:
3524 3633 self.obj_ref_id = 0
3525 3634 self.choice_sensor.Show(self.obj_ref_id)
  3635 +
3526 3636 for m in range(0, 5):
3527 3637 self.obj_fiducials[m, :] = np.full([1, 3], np.nan)
3528 3638 self.obj_orients[m, :] = np.full([1, 3], np.nan)
... ... @@ -3539,7 +3649,7 @@ class ObjectCalibrationDialog(wx.Dialog):
3539 3649 self.obj_ref_id = 0
3540 3650  
3541 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 3654 class ICPCorregistrationDialog(wx.Dialog):
3545 3655  
... ...
invesalius/gui/task_navigator.py
... ... @@ -17,11 +17,11 @@
17 17 # detalhes.
18 18 #--------------------------------------------------------------------------
19 19  
  20 +import dataclasses
20 21 from functools import partial
  22 +import itertools
21 23 import csv
22   -import os
23 24 import queue
24   -import sys
25 25 import time
26 26 import threading
27 27  
... ... @@ -42,10 +42,8 @@ except ImportError:
42 42 import wx
43 43  
44 44 try:
45   - import wx.lib.agw.hyperlink as hl
46 45 import wx.lib.agw.foldpanelbar as fpb
47 46 except ImportError:
48   - import wx.lib.hyperlink as hl
49 47 import wx.lib.foldpanelbar as fpb
50 48  
51 49 import wx.lib.colourselect as csel
... ... @@ -54,23 +52,22 @@ from invesalius.pubsub import pub as Publisher
54 52 from time import sleep
55 53  
56 54 import invesalius.constants as const
57   -import invesalius.data.bases as db
58 55  
59 56 if has_trekker:
60 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 60 import invesalius.data.slice_ as sl
65   -import invesalius.data.trackers as dt
66 61 import invesalius.data.tractography as dti
67   -import invesalius.data.transformations as tr
68   -import invesalius.data.trigger as trig
69 62 import invesalius.data.record_coords as rec
70 63 import invesalius.data.vtk_utils as vtk_utils
71 64 import invesalius.gui.dialogs as dlg
72 65 import invesalius.project as prj
73 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 72 HAS_PEDAL_CONNECTION = True
76 73 try:
... ... @@ -165,6 +162,12 @@ class InnerFoldPanel(wx.Panel):
165 162  
166 163 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
167 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 171 # Fold panel style
169 172 style = fpb.CaptionBarStyle()
170 173 style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V)
... ... @@ -173,7 +176,7 @@ class InnerFoldPanel(wx.Panel):
173 176  
174 177 # Fold 1 - Navigation panel
175 178 item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True)
176   - ntw = NeuronavigationPanel(item)
  179 + ntw = NeuronavigationPanel(item, tracker, pedal_connection)
177 180  
178 181 fold_panel.ApplyCaptionStyle(item, style)
179 182 fold_panel.AddFoldPanelWindow(item, ntw, spacing=0,
... ... @@ -182,7 +185,7 @@ class InnerFoldPanel(wx.Panel):
182 185  
183 186 # Fold 2 - Object registration panel
184 187 item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True)
185   - otw = ObjectRegistrationPanel(item)
  188 + otw = ObjectRegistrationPanel(item, tracker, pedal_connection)
186 189  
187 190 fold_panel.ApplyCaptionStyle(item, style)
188 191 fold_panel.AddFoldPanelWindow(item, otw, spacing=0,
... ... @@ -222,13 +225,13 @@ class InnerFoldPanel(wx.Panel):
222 225 checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCamera)
223 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 236 # Check box for object position and orientation update in volume rendering during navigation
234 237 tooltip = wx.ToolTip(_("Show and track TMS coil"))
... ... @@ -241,12 +244,12 @@ class InnerFoldPanel(wx.Panel):
241 244  
242 245 # if sys.platform != 'win32':
243 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 248 checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
246 249  
247 250 line_sizer = wx.BoxSizer(wx.HORIZONTAL)
248 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 253 line_sizer.Add(checkobj, 0, wx.RIGHT | wx.LEFT, 5)
251 254 line_sizer.Fit(self)
252 255  
... ... @@ -277,17 +280,24 @@ class InnerFoldPanel(wx.Panel):
277 280  
278 281 def OnCheckStatus(self, nav_status, vis_status):
279 282 if nav_status:
280   - self.checktrigger.Enable(False)
  283 + self.checkbox_serial_port.Enable(False)
281 284 self.checkobj.Enable(False)
282 285 else:
283   - self.checktrigger.Enable(True)
  286 + self.checkbox_serial_port.Enable(True)
284 287 if self.track_obj:
285 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 301 if not evt:
292 302 if flag:
293 303 self.checkobj.Enable(True)
... ... @@ -308,432 +318,8 @@ class InnerFoldPanel(wx.Panel):
308 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 321 class NeuronavigationPanel(wx.Panel):
736   - def __init__(self, parent):
  322 + def __init__(self, parent, tracker, pedal_connection):
737 323 wx.Panel.__init__(self, parent)
738 324 try:
739 325 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
... ... @@ -746,14 +332,18 @@ class NeuronavigationPanel(wx.Panel):
746 332 self.__bind_events()
747 333  
748 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 339 self.icp = ICP()
  340 + self.tracker = tracker
753 341 self.robot = Robot()
754 342 self.robotcoordinates = elfin_process.RobotCoordinates()
755 343  
756 344 self.nav_status = False
  345 + self.tracker_fiducial_being_set = None
  346 + self.current_coord = 0, 0, 0
757 347  
758 348 # Initialize list of buttons and numctrls for wx objects
759 349 self.btns_set_fiducial = [None, None, None, None, None, None]
... ... @@ -761,7 +351,7 @@ class NeuronavigationPanel(wx.Panel):
761 351  
762 352 # ComboBox for spatial tracker device selection
763 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 355 choices=tracker_options, style=wx.CB_DROPDOWN|wx.CB_READONLY)
766 356  
767 357 tooltip = wx.ToolTip(_("Choose the tracking device"))
... ... @@ -786,9 +376,12 @@ class NeuronavigationPanel(wx.Panel):
786 376 label = fiducial['label']
787 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 386 # Push buttons for tracker fiducials
794 387 for n, fiducial in enumerate(const.TRACKER_FIDUCIALS):
... ... @@ -796,15 +389,18 @@ class NeuronavigationPanel(wx.Panel):
796 389 label = fiducial['label']
797 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 399 # TODO: Find a better allignment between FRE, text and navigate button
804 400 txt_fre = wx.StaticText(self, -1, _('FRE:'))
805 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 404 txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:'))
809 405 else:
810 406 txt_pedal_pressed = None
... ... @@ -833,14 +429,14 @@ class NeuronavigationPanel(wx.Panel):
833 429 self.checkbox_icp = checkbox_icp
834 430  
835 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 433 tooltip = wx.ToolTip(_(u"Is the pedal pressed"))
838 434 checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' '))
839 435 checkbox_pedal_pressed.SetValue(False)
840 436 checkbox_pedal_pressed.Enable(False)
841 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 441 self.checkbox_pedal_pressed = checkbox_pedal_pressed
846 442 else:
... ... @@ -874,7 +470,7 @@ class NeuronavigationPanel(wx.Panel):
874 470 (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
875 471  
876 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 474 pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL),
879 475 (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)])
880 476  
... ... @@ -899,7 +495,7 @@ class NeuronavigationPanel(wx.Panel):
899 495 Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials')
900 496 Publisher.subscribe(self.SetImageFiducial, 'Set image fiducial')
901 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 499 Publisher.subscribe(self.UpdateTrackObjectState, 'Update track object state')
904 500 Publisher.subscribe(self.UpdateImageCoordinates, 'Set cross focal point')
905 501 Publisher.subscribe(self.OnDisconnectTracker, 'Disconnect tracker')
... ... @@ -920,14 +516,14 @@ class NeuronavigationPanel(wx.Panel):
920 516 Publisher.subscribe(self.OnStartNavigation, 'Start navigation')
921 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 522 fiducial_index = fiducial['fiducial_index']
927 523 fiducial_name = fiducial['fiducial_name']
928 524  
929 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 527 return
932 528  
933 529 Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=coord[0:3])
... ... @@ -956,7 +552,12 @@ class NeuronavigationPanel(wx.Panel):
956 552 fiducial = self.GetFiducialByAttribute(const.TRACKER_FIDUCIALS, 'fiducial_name', fiducial_name)
957 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 562 self.ResetICP()
962 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 587 self.navigation.seed_radius = data
987 588  
988 589 def UpdateSleep(self, data):
989   - self.navigation.sleep_nav = data
  590 + self.navigation.UpdateSleep(data)
990 591  
991 592 def UpdateNumberThreads(self, data):
992 593 self.navigation.n_threads = data
... ... @@ -1005,7 +606,7 @@ class NeuronavigationPanel(wx.Panel):
1005 606  
1006 607 def UpdateImageCoordinates(self, position):
1007 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 611 for m in [0, 1, 2]:
1011 612 if not self.btns_set_fiducial[m].GetValue():
... ... @@ -1015,11 +616,11 @@ class NeuronavigationPanel(wx.Panel):
1015 616 def UpdateObjectRegistration(self, data=None):
1016 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 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 625 def ResetICP(self):
1025 626 self.icp.ResetICP()
... ... @@ -1032,19 +633,14 @@ class NeuronavigationPanel(wx.Panel):
1032 633 self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre)
1033 634  
1034 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 638 if hasattr(evt, 'GetSelection'):
1038 639 choice = evt.GetSelection()
1039 640 else:
1040 641 choice = None
1041 642  
1042 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 644 if self.tracker.tracker_id == const.ROBOT:
1049 645 self.robot.SetRobotQueues([self.navigation.robottarget_queue,
1050 646 self.navigation.objattarget_queue])
... ... @@ -1060,7 +656,14 @@ class NeuronavigationPanel(wx.Panel):
1060 656 Publisher.sendMessage('Update status text in GUI', label=_("Ready"))
1061 657  
1062 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 667 self.ResetICP()
1065 668  
1066 669 print("Reference mode changed!")
... ... @@ -1069,7 +672,7 @@ class NeuronavigationPanel(wx.Panel):
1069 672 fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name']
1070 673  
1071 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 677 if self.btns_set_fiducial[n].GetValue():
1075 678 coord = self.numctrls_fiducial[n][0].GetValue(),\
... ... @@ -1083,17 +686,40 @@ class NeuronavigationPanel(wx.Panel):
1083 686 seed = 3 * [0.]
1084 687  
1085 688 Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size,
1086   - marker_id=marker_id, seed=seed)
  689 + label=label, seed=seed)
1087 690 else:
1088 691 for m in [0, 1, 2]:
1089 692 self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m]))
1090 693  
1091 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 724 def OnStopNavigation(self):
1099 725 select_tracker_elem = self.select_tracker_elem
... ... @@ -1197,14 +823,16 @@ class NeuronavigationPanel(wx.Panel):
1197 823 # TODO: Reset camera initial focus
1198 824 Publisher.sendMessage('Reset cam clipping range')
1199 825 self.navigation.StopNavigation()
1200   - self.navigation.__init__()
  826 + self.navigation.__init__(
  827 + pedal_connection=self.pedal_connection,
  828 + )
1201 829 self.tracker.__init__()
1202 830 self.icp.__init__()
1203 831 self.robot.__init__()
1204 832  
1205 833  
1206 834 class ObjectRegistrationPanel(wx.Panel):
1207   - def __init__(self, parent):
  835 + def __init__(self, parent, tracker, pedal_connection):
1208 836 wx.Panel.__init__(self, parent)
1209 837 try:
1210 838 default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR)
... ... @@ -1214,6 +842,9 @@ class ObjectRegistrationPanel(wx.Panel):
1214 842  
1215 843 self.coil_list = const.COIL
1216 844  
  845 + self.tracker = tracker
  846 + self.pedal_connection = pedal_connection
  847 +
1217 848 self.nav_prop = None
1218 849 self.obj_fiducials = None
1219 850 self.obj_orients = None
... ... @@ -1323,14 +954,10 @@ class ObjectRegistrationPanel(wx.Panel):
1323 954 self.Update()
1324 955  
1325 956 def __bind_events(self):
1326   - Publisher.subscribe(self.UpdateTrackerInit, 'Update tracker initializer')
1327 957 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
1328 958 Publisher.subscribe(self.OnCloseProject, 'Close project data')
1329 959 Publisher.subscribe(self.OnRemoveObject, 'Remove object data')
1330 960  
1331   - def UpdateTrackerInit(self, nav_prop):
1332   - self.nav_prop = nav_prop
1333   -
1334 961 def UpdateNavigationStatus(self, nav_status, vis_status):
1335 962 if nav_status:
1336 963 self.checkrecordcoords.Enable(1)
... ... @@ -1378,11 +1005,11 @@ class ObjectRegistrationPanel(wx.Panel):
1378 1005  
1379 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 1010 try:
1384 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 1013 if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all():
1387 1014 self.checktrack.Enable(1)
1388 1015 Publisher.sendMessage('Update object registration',
... ... @@ -1391,7 +1018,13 @@ class ObjectRegistrationPanel(wx.Panel):
1391 1018 label=_("Ready"))
1392 1019 # Enable automatically Track object, Show coil and disable Vol. Camera
1393 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 1028 Publisher.sendMessage('Change camera checkbox', status=False)
1396 1029  
1397 1030 except wx._core.PyAssertionError: # TODO FIX: win64
... ... @@ -1468,6 +1101,76 @@ class ObjectRegistrationPanel(wx.Panel):
1468 1101  
1469 1102  
1470 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 1174 def __init__(self, parent):
1472 1175 wx.Panel.__init__(self, parent)
1473 1176 try:
... ... @@ -1483,6 +1186,7 @@ class MarkersPanel(wx.Panel):
1483 1186 self.current_coord = 0, 0, 0, 0, 0, 0
1484 1187 self.current_angle = 0, 0, 0
1485 1188 self.current_seed = 0, 0, 0
  1189 + self.markers = []
1486 1190 self.current_ref = 0, 0, 0, 0, 0, 0
1487 1191 self.current_robot = 0, 0, 0, 0, 0, 0
1488 1192 self.list_coord = []
... ... @@ -1492,12 +1196,12 @@ class MarkersPanel(wx.Panel):
1492 1196 self.mchange = None
1493 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 1199 self.marker_colour = const.MARKER_COLOUR
1499 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 1205 # Change marker size
1502 1206 spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23))
1503 1207 spin_size.SetRange(1, 99)
... ... @@ -1534,7 +1238,7 @@ class MarkersPanel(wx.Panel):
1534 1238  
1535 1239 # Buttons to delete or remove markers
1536 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 1243 btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23))
1540 1244 btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers)
... ... @@ -1550,11 +1254,15 @@ class MarkersPanel(wx.Panel):
1550 1254 self.lc.InsertColumn(2, 'Y')
1551 1255 self.lc.InsertColumn(3, 'Z')
1552 1256 self.lc.InsertColumn(4, 'ID')
  1257 + self.lc.InsertColumn(5, 'Target')
  1258 +
1553 1259 self.lc.SetColumnWidth(0, 28)
1554 1260 self.lc.SetColumnWidth(1, 50)
1555 1261 self.lc.SetColumnWidth(2, 50)
1556 1262 self.lc.SetColumnWidth(3, 50)
1557 1263 self.lc.SetColumnWidth(4, 60)
  1264 + self.lc.SetColumnWidth(5, 60)
  1265 +
1558 1266 self.lc.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown)
1559 1267 self.lc.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink)
1560 1268 self.lc.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnStopItemBlink)
... ... @@ -1573,7 +1281,7 @@ class MarkersPanel(wx.Panel):
1573 1281 def __bind_events(self):
1574 1282 # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points')
1575 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 1285 Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers')
1578 1286 Publisher.subscribe(self.CreateMarker, 'Create marker')
1579 1287 Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status')
... ... @@ -1584,6 +1292,70 @@ class MarkersPanel(wx.Panel):
1584 1292 Publisher.subscribe(self.UpdateObjectMarker2Center, 'Update object marker to center')
1585 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 1359 def UpdateCurrentCoord(self, position):
1588 1360 self.current_coord = position
1589 1361 #self.current_angle = pubsub_evt.data[1][3:]
... ... @@ -1620,7 +1392,7 @@ class MarkersPanel(wx.Panel):
1620 1392 # TODO: Enable the "Set as target" only when target is created with registered object
1621 1393 menu_id = wx.Menu()
1622 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 1396 color_id = menu_id.Append(2, _('Edit color'))
1625 1397 menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id)
1626 1398 menu_id.AppendSeparator()
... ... @@ -1644,54 +1416,29 @@ class MarkersPanel(wx.Panel):
1644 1416 def OnStopItemBlink(self, evt):
1645 1417 Publisher.sendMessage('Stop Blink Marker')
1646 1418  
1647   - def OnMenuEditMarkerId(self, evt):
  1419 + def OnMenuEditMarkerLabel(self, evt):
1648 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 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 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 1435 def OnMenuSetColor(self, evt):
1692 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 1443 color_new = dlg.ShowColorDialog(color_current=color_current)
1697 1444  
... ... @@ -1701,7 +1448,7 @@ class MarkersPanel(wx.Panel):
1701 1448 # XXX: Seems like a slightly too early point for rounding; better to round only when the value
1702 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 1453 Publisher.sendMessage('Set new color', index=index, color=color_new)
1707 1454  
... ... @@ -1738,151 +1485,95 @@ class MarkersPanel(wx.Panel):
1738 1485  
1739 1486  
1740 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 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 1514 for id_n in range(self.lc.GetItemCount()):
1768 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 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 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 1531 #TODO: reset robot target. (target should be the same target as invesalius?)
1786 1532 Publisher.sendMessage('Robot target matrix', robot_tracker_flag=False,
1787 1533 m_change_robot2ref=None)
1788 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 1537 self.CreateMarker()
1805 1538  
1806 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 1543 filename = dlg.ShowLoadSaveDialog(message=_(u"Load markers"),
1808 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 1578 def OnMarkersVisibility(self, evt, ctrl):
1888 1579  
... ... @@ -1905,79 +1596,55 @@ class MarkersPanel(wx.Panel):
1905 1596 filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."),
1906 1597 wildcard=const.WILDCARD_MARKER_FILES,
1907 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 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 1618 def OnSelectSize(self, evt, ctrl):
1927 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 1639 # Add item to list control in panel
1961 1640 num_items = self.lc.GetItemCount()
1962 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 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 1648 class DbsPanel(wx.Panel):
1982 1649 def __init__(self, parent):
1983 1650 wx.Panel.__init__(self, parent)
... ... @@ -2429,101 +2096,6 @@ class TractographyPanel(wx.Panel):
2429 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 2099 class InputAttributes(object):
2528 2100 # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python
2529 2101 def __init__(self, *initial_data, **kwargs):
... ...
invesalius/navigation/icp.py 0 → 100644
... ... @@ -0,0 +1,80 @@
  1 +#--------------------------------------------------------------------------
  2 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  3 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  4 +# Homepage: http://www.softwarepublico.gov.br
  5 +# Contact: invesalius@cti.gov.br
  6 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  7 +#--------------------------------------------------------------------------
  8 +# Este programa e software livre; voce pode redistribui-lo e/ou
  9 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  10 +# publicada pela Free Software Foundation; de acordo com a versao 2
  11 +# da Licenca.
  12 +#
  13 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  14 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  15 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  16 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  17 +# detalhes.
  18 +#--------------------------------------------------------------------------
  19 +
  20 +import wx
  21 +
  22 +import invesalius.data.bases as db
  23 +import invesalius.gui.dialogs as dlg
  24 +from invesalius.pubsub import pub as Publisher
  25 +
  26 +
  27 +class ICP():
  28 + def __init__(self):
  29 + self.use_icp = False
  30 + self.m_icp = None
  31 + self.icp_fre = None
  32 +
  33 + def StartICP(self, navigation, tracker):
  34 + ref_mode_id = navigation.GetReferenceMode()
  35 +
  36 + if not self.use_icp:
  37 + if dlg.ICPcorregistration(navigation.fre):
  38 + Publisher.sendMessage('Stop navigation')
  39 + use_icp, self.m_icp = self.OnICP(navigation, tracker, navigation.m_change)
  40 + if use_icp:
  41 + self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials,
  42 + ref_mode_id, navigation.m_change, self.m_icp)
  43 + self.SetICP(navigation, use_icp)
  44 + else:
  45 + print("ICP canceled")
  46 + Publisher.sendMessage('Start navigation')
  47 +
  48 + def OnICP(self, navigation, tracker, m_change):
  49 + ref_mode_id = navigation.GetReferenceMode()
  50 +
  51 + dialog = dlg.ICPCorregistrationDialog(nav_prop=(m_change, tracker, ref_mode_id))
  52 +
  53 + if dialog.ShowModal() == wx.ID_OK:
  54 + m_icp, point_coord, transformed_points, prev_error, final_error = dialog.GetValue()
  55 + # TODO: checkbox in the dialog to transfer the icp points to 3D viewer
  56 + #create markers
  57 + # for i in range(len(point_coord)):
  58 + # img_coord = point_coord[i][0],-point_coord[i][1],point_coord[i][2], 0, 0, 0
  59 + # transf_coord = transformed_points[i][0],-transformed_points[i][1],transformed_points[i][2], 0, 0, 0
  60 + # Publisher.sendMessage('Create marker', coord=img_coord, marker_id=None, colour=(1,0,0))
  61 + # Publisher.sendMessage('Create marker', coord=transf_coord, marker_id=None, colour=(0,0,1))
  62 + if m_icp is not None:
  63 + dlg.ReportICPerror(prev_error, final_error)
  64 + use_icp = True
  65 + else:
  66 + use_icp = False
  67 +
  68 + return use_icp, m_icp
  69 +
  70 + else:
  71 + return self.use_icp, self.m_icp
  72 +
  73 + def SetICP(self, navigation, use_icp):
  74 + self.use_icp = use_icp
  75 + navigation.icp_queue.put_nowait([self.use_icp, self.m_icp])
  76 +
  77 + def ResetICP(self):
  78 + self.use_icp = False
  79 + self.m_icp = None
  80 + self.icp_fre = None
... ...
invesalius/navigation/navigation.py 0 → 100644
... ... @@ -0,0 +1,341 @@
  1 +#--------------------------------------------------------------------------
  2 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  3 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  4 +# Homepage: http://www.softwarepublico.gov.br
  5 +# Contact: invesalius@cti.gov.br
  6 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  7 +#--------------------------------------------------------------------------
  8 +# Este programa e software livre; voce pode redistribui-lo e/ou
  9 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  10 +# publicada pela Free Software Foundation; de acordo com a versao 2
  11 +# da Licenca.
  12 +#
  13 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  14 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  15 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  16 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  17 +# detalhes.
  18 +#--------------------------------------------------------------------------
  19 +
  20 +import threading
  21 +import queue
  22 +from time import sleep
  23 +
  24 +import wx
  25 +import numpy as np
  26 +
  27 +import invesalius.constants as const
  28 +import invesalius.project as prj
  29 +import invesalius.data.bases as db
  30 +import invesalius.data.coordinates as dco
  31 +import invesalius.data.coregistration as dcr
  32 +import invesalius.data.serial_port_connection as spc
  33 +import invesalius.data.slice_ as sl
  34 +import invesalius.data.tractography as dti
  35 +import invesalius.data.transformations as tr
  36 +import invesalius.data.vtk_utils as vtk_utils
  37 +from invesalius.pubsub import pub as Publisher
  38 +
  39 +
  40 +class QueueCustom(queue.Queue):
  41 + """
  42 + A custom queue subclass that provides a :meth:`clear` method.
  43 + https://stackoverflow.com/questions/6517953/clear-all-items-from-the-queue
  44 + Modified to a LIFO Queue type (Last-in-first-out). Seems to make sense for the navigation
  45 + threads, as the last added coordinate should be the first to be processed.
  46 + In the first tests in a short run, seems to increase the coord queue size considerably,
  47 + possibly limiting the queue size is good.
  48 + """
  49 +
  50 + def clear(self):
  51 + """
  52 + Clears all items from the queue.
  53 + """
  54 +
  55 + with self.mutex:
  56 + unfinished = self.unfinished_tasks - len(self.queue)
  57 + if unfinished <= 0:
  58 + if unfinished < 0:
  59 + raise ValueError('task_done() called too many times')
  60 + self.all_tasks_done.notify_all()
  61 + self.unfinished_tasks = unfinished
  62 + self.queue.clear()
  63 + self.not_full.notify_all()
  64 +
  65 +
  66 +class UpdateNavigationScene(threading.Thread):
  67 +
  68 + def __init__(self, vis_queues, vis_components, event, sle):
  69 + """Class (threading) to update the navigation scene with all graphical elements.
  70 +
  71 + Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation
  72 +
  73 + :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene
  74 + :type affine_vtk: vtkMatrix4x4
  75 + :param visualization_queue: Queue instance that manage coordinates to be visualized
  76 + :type visualization_queue: queue.Queue
  77 + :param event: Threading event to coordinate when tasks as done and allow UI release
  78 + :type event: threading.Event
  79 + :param sle: Sleep pause in seconds
  80 + :type sle: float
  81 + """
  82 +
  83 + threading.Thread.__init__(self, name='UpdateScene')
  84 + self.serial_port_enabled, self.view_tracts, self.peel_loaded = vis_components
  85 + self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue = vis_queues
  86 + self.sle = sle
  87 + self.event = event
  88 +
  89 + def run(self):
  90 + # count = 0
  91 + while not self.event.is_set():
  92 + got_coords = False
  93 + try:
  94 + coord, m_img, view_obj = self.coord_queue.get_nowait()
  95 + got_coords = True
  96 +
  97 + # print('UpdateScene: get {}'.format(count))
  98 +
  99 + # use of CallAfter is mandatory otherwise crashes the wx interface
  100 + if self.view_tracts:
  101 + bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait()
  102 + #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command
  103 + wx.CallAfter(Publisher.sendMessage, 'Remove tracts')
  104 + wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle,
  105 + affine_vtk=affine_vtk, coord_offset=coord_offset)
  106 + # wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset)
  107 + self.tracts_queue.task_done()
  108 +
  109 + if self.serial_port_enabled:
  110 + trigger_on = self.serial_port_queue.get_nowait()
  111 + if trigger_on:
  112 + wx.CallAfter(Publisher.sendMessage, 'Create marker')
  113 + self.serial_port_queue.task_done()
  114 +
  115 + #TODO: If using the view_tracts substitute the raw coord from the offset coordinate, so the user
  116 + # see the red cross in the position of the offset marker
  117 + wx.CallAfter(Publisher.sendMessage, 'Update slices position', position=coord[:3])
  118 + wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord)
  119 + wx.CallAfter(Publisher.sendMessage, 'Update slice viewer')
  120 +
  121 + if view_obj:
  122 + wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord)
  123 + wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix',m_img=m_img, coord=coord, flag= self.peel_loaded)
  124 + self.coord_queue.task_done()
  125 + # print('UpdateScene: done {}'.format(count))
  126 + # count += 1
  127 +
  128 + sleep(self.sle)
  129 + except queue.Empty:
  130 + if got_coords:
  131 + self.coord_queue.task_done()
  132 +
  133 +
  134 +class Navigation():
  135 + def __init__(self, pedal_connection):
  136 + self.pedal_connection = pedal_connection
  137 +
  138 + self.image_fiducials = np.full([3, 3], np.nan)
  139 + self.correg = None
  140 + self.target = None
  141 + self.obj_reg = None
  142 + self.track_obj = False
  143 + self.m_change = None
  144 + self.all_fiducials = np.zeros((6, 6))
  145 +
  146 + self.event = threading.Event()
  147 + self.coord_queue = QueueCustom(maxsize=1)
  148 + self.icp_queue = QueueCustom(maxsize=1)
  149 + # self.visualization_queue = QueueCustom(maxsize=1)
  150 + self.serial_port_queue = QueueCustom(maxsize=1)
  151 + self.coord_tracts_queue = QueueCustom(maxsize=1)
  152 + self.tracts_queue = QueueCustom(maxsize=1)
  153 +
  154 + # Tracker parameters
  155 + self.ref_mode_id = const.DEFAULT_REF_MODE
  156 +
  157 + # Tractography parameters
  158 + self.trk_inp = None
  159 + self.trekker = None
  160 + self.n_threads = None
  161 + self.view_tracts = False
  162 + self.peel_loaded = False
  163 + self.enable_act = False
  164 + self.act_data = None
  165 + self.n_tracts = const.N_TRACTS
  166 + self.seed_offset = const.SEED_OFFSET
  167 + self.seed_radius = const.SEED_RADIUS
  168 + self.sleep_nav = const.SLEEP_NAVIGATION
  169 +
  170 + # Serial port
  171 + self.serial_port = None
  172 + self.serial_port_connection = None
  173 +
  174 + # During navigation
  175 + self.coil_at_target = False
  176 +
  177 + self.__bind_events()
  178 +
  179 + def __bind_events(self):
  180 + Publisher.subscribe(self.CoilAtTarget, 'Coil at target')
  181 +
  182 + def CoilAtTarget(self, state):
  183 + self.coil_at_target = state
  184 +
  185 + def UpdateSleep(self, sleep):
  186 + self.sleep_nav = sleep
  187 + self.serial_port_connection.sleep_nav = sleep
  188 +
  189 + def SerialPortEnabled(self):
  190 + return self.serial_port is not None
  191 +
  192 + def SetReferenceMode(self, value):
  193 + self.ref_mode_id = value
  194 +
  195 + def GetReferenceMode(self):
  196 + return self.ref_mode_id
  197 +
  198 + def SetImageFiducial(self, fiducial_index, coord):
  199 + self.image_fiducials[fiducial_index, :] = coord
  200 +
  201 + print("Set image fiducial {} to coordinates {}".format(fiducial_index, coord))
  202 +
  203 + def AreImageFiducialsSet(self):
  204 + return not np.isnan(self.image_fiducials).any()
  205 +
  206 + def UpdateFiducialRegistrationError(self, tracker):
  207 + tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()
  208 +
  209 + self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])
  210 +
  211 + self.fre = db.calculate_fre(tracker_fiducials_raw, self.all_fiducials, self.ref_mode_id, self.m_change)
  212 +
  213 + def GetFiducialRegistrationError(self, icp):
  214 + fre = icp.icp_fre if icp.use_icp else self.fre
  215 + return fre, fre <= const.FIDUCIAL_REGISTRATION_ERROR_THRESHOLD
  216 +
  217 + def PedalStateChanged(self, state):
  218 + if state is True and self.coil_at_target and self.SerialPortEnabled():
  219 + self.serial_port_connection.SendPulse()
  220 +
  221 + def StartNavigation(self, tracker):
  222 + tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials()
  223 +
  224 + # initialize jobs list
  225 + jobs_list = []
  226 +
  227 + if self.event.is_set():
  228 + self.event.clear()
  229 +
  230 + vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded]
  231 + vis_queues = [self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue]
  232 +
  233 + Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components)
  234 +
  235 + self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials])
  236 +
  237 + # fiducials matrix
  238 + m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T,
  239 + shear=False, scale=False)
  240 + self.m_change = m_change
  241 +
  242 + errors = False
  243 +
  244 + if self.track_obj:
  245 + # if object tracking is selected
  246 + if self.obj_reg is None:
  247 + # check if object registration was performed
  248 + wx.MessageBox(_("Perform coil registration before navigation."), _("InVesalius 3"))
  249 + errors = True
  250 + else:
  251 + # if object registration was correctly performed continue with navigation
  252 + # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix
  253 + obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg
  254 +
  255 + coreg_data = [m_change, obj_ref_mode]
  256 +
  257 + if self.ref_mode_id:
  258 + coord_raw, 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 @@
  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 22  
23 23 import mido
24 24  
  25 +from invesalius.pubsub import pub as Publisher
25 26 from invesalius.utils import Singleton
26 27  
27 28 class PedalConnection(Thread, metaclass=Singleton):
... ... @@ -50,6 +51,8 @@ class PedalConnection(Thread, metaclass=Singleton):
50 51 if not self._callbacks:
51 52 print("Pedal pressed, no callbacks registered")
52 53 else:
  54 + Publisher.sendMessage('Pedal state changed', state=True)
  55 +
53 56 for callback in self._callbacks.values():
54 57 callback(True)
55 58  
... ... @@ -57,9 +60,10 @@ class PedalConnection(Thread, metaclass=Singleton):
57 60 if not self._callbacks:
58 61 print("Pedal released, no callbacks registered")
59 62 else:
  63 + Publisher.sendMessage('Pedal state changed', state=False)
  64 +
60 65 for callback in self._callbacks.values():
61 66 callback(False)
62   -
63 67 else:
64 68 print("Unknown message type received from MIDI device")
65 69  
... ... @@ -70,6 +74,8 @@ class PedalConnection(Thread, metaclass=Singleton):
70 74 self._midi_in._rt.ignore_types(False, False, False)
71 75 self._midi_in.callback = self._midi_to_pedal
72 76  
  77 + Publisher.sendMessage('Pedal connection', state=True)
  78 +
73 79 print("Connected to MIDI device")
74 80  
75 81 def _check_disconnected(self):
... ... @@ -77,6 +83,8 @@ class PedalConnection(Thread, metaclass=Singleton):
77 83 if self._active_input not in self._midi_inputs:
78 84 self._midi_in = None
79 85  
  86 + Publisher.sendMessage('Pedal connection', state=False)
  87 +
80 88 print("Disconnected from MIDI device")
81 89  
82 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 3 Pypubsub==4.0.3
4 4 configparser==5.0.1
5 5 h5py==2.10.0
6 6 imageio==2.9.0
7 7 nibabel==3.2.1
8   -numpy==1.20.1
  8 +numpy==1.21.2
9 9 plaidml-keras==0.7.0
10 10 psutil==5.8.0
11 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 17 Theano==1.0.5
... ...