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