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