Commit af27358df57f107ac4f4e4b5830a582dc614d2ea
Committed by
GitHub
Exists in
master
Merge branch 'master' into export-world-coordinates-into-marker-file
Showing
9 changed files
with
1058 additions
and
613 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 | + | |
12 | +# User-specific stuff | |
13 | +.idea/**/workspace.xml | |
14 | +.idea/**/tasks.xml | |
15 | +.idea/**/usage.statistics.xml | |
16 | +.idea/**/dictionaries | |
17 | +.idea/**/shelf | |
18 | + | |
19 | +# AWS User-specific | |
20 | +.idea/**/aws.xml | |
21 | + | |
22 | +# Generated files | |
23 | +.idea/**/contentModel.xml | |
24 | + | |
25 | +# Sensitive or high-churn files | |
26 | +.idea/**/dataSources/ | |
27 | +.idea/**/dataSources.ids | |
28 | +.idea/**/dataSources.local.xml | |
29 | +.idea/**/sqlDataSources.xml | |
30 | +.idea/**/dynamic.xml | |
31 | +.idea/**/uiDesigner.xml | |
32 | +.idea/**/dbnavigator.xml | |
33 | + | |
34 | +# Gradle | |
35 | +.idea/**/gradle.xml | |
36 | +.idea/**/libraries | |
37 | + | |
38 | +# Gradle and Maven with auto-import | |
39 | +# When using Gradle or Maven with auto-import, you should exclude module files, | |
40 | +# since they will be recreated, and may cause churn. Uncomment if using | |
41 | +# auto-import. | |
42 | +# .idea/artifacts | |
43 | +# .idea/compiler.xml | |
44 | +# .idea/jarRepositories.xml | |
45 | +# .idea/modules.xml | |
46 | +# .idea/*.iml | |
47 | +# .idea/modules | |
48 | +# *.iml | |
49 | +# *.ipr | |
50 | + | |
51 | +# CMake | |
52 | +cmake-build-*/ | |
53 | + | |
54 | +# Mongo Explorer plugin | |
55 | +.idea/**/mongoSettings.xml | |
56 | + | |
57 | +# File-based project format | |
58 | +*.iws | |
59 | + | |
60 | +# IntelliJ | |
61 | +out/ | |
62 | + | |
63 | +# mpeltonen/sbt-idea plugin | |
64 | +.idea_modules/ | |
65 | + | |
66 | +# JIRA plugin | |
67 | +atlassian-ide-plugin.xml | |
68 | + | |
69 | +# Cursive Clojure plugin | |
70 | +.idea/replstate.xml | |
71 | + | |
72 | +# Crashlytics plugin (for Android Studio and IntelliJ) | |
73 | +com_crashlytics_export_strings.xml | |
74 | +crashlytics.properties | |
75 | +crashlytics-build.properties | |
76 | +fabric.properties | |
77 | + | |
78 | +# Editor-based Rest Client | |
79 | +.idea/httpRequests | |
80 | + | |
81 | +# Android studio 3.1+ serialized cache file | |
82 | +.idea/caches/build_file_checksums.ser | |
83 | + | |
84 | +### Intellij Patch ### | |
85 | +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 | |
86 | + | |
87 | +# *.iml | |
88 | +# modules.xml | |
89 | +# .idea/misc.xml | |
90 | +# *.ipr | |
91 | + | |
92 | +# Sonarlint plugin | |
93 | +# https://plugins.jetbrains.com/plugin/7973-sonarlint | |
94 | +.idea/**/sonarlint/ | |
95 | + | |
96 | +# SonarQube Plugin | |
97 | +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin | |
98 | +.idea/**/sonarIssues.xml | |
99 | + | |
100 | +# Markdown Navigator plugin | |
101 | +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced | |
102 | +.idea/**/markdown-navigator.xml | |
103 | +.idea/**/markdown-navigator-enh.xml | |
104 | +.idea/**/markdown-navigator/ | |
105 | + | |
106 | +# Cache file creation bug | |
107 | +# See https://youtrack.jetbrains.com/issue/JBR-2257 | |
108 | +.idea/$CACHE_FILE$ | |
109 | + | |
110 | +# CodeStream plugin | |
111 | +# https://plugins.jetbrains.com/plugin/12206-codestream | |
112 | +.idea/codestream.xml | |
113 | + | |
114 | +### Python ### | |
115 | +# Byte-compiled / optimized / DLL files | |
116 | +__pycache__/ | |
117 | +*.py[cod] | |
118 | +*$py.class | |
119 | + | |
120 | +# C extensions | |
23 | 121 | *.so |
24 | -tags | |
25 | -*.c | |
26 | 122 | |
27 | -.idea | |
28 | -build | |
29 | -*.patch | |
30 | -*.tgz | |
123 | +# Distribution / packaging | |
124 | +.Python | |
125 | +build/ | |
126 | +develop-eggs/ | |
127 | +dist/ | |
128 | +downloads/ | |
129 | +eggs/ | |
130 | +.eggs/ | |
131 | +lib/ | |
132 | +lib64/ | |
133 | +parts/ | |
134 | +sdist/ | |
135 | +var/ | |
136 | +wheels/ | |
137 | +share/python-wheels/ | |
138 | +*.egg-info/ | |
139 | +.installed.cfg | |
140 | +*.egg | |
141 | +MANIFEST | |
31 | 142 | |
32 | -*.pyd | |
33 | -*.cpp | |
34 | -*.diff | |
143 | +# PyInstaller | |
144 | +# Usually these files are written by a python script from a template | |
145 | +# before PyInstaller builds the exe, so as to inject date/other infos into it. | |
146 | +*.manifest | |
147 | +*.spec | |
148 | + | |
149 | +# Installer logs | |
150 | +pip-log.txt | |
151 | +pip-delete-this-directory.txt | |
152 | + | |
153 | +# Unit test / coverage reports | |
154 | +htmlcov/ | |
155 | +.tox/ | |
156 | +.nox/ | |
157 | +.coverage | |
158 | +.coverage.* | |
159 | +.cache | |
160 | +nosetests.xml | |
161 | +coverage.xml | |
162 | +*.cover | |
163 | +*.py,cover | |
164 | +.hypothesis/ | |
165 | +.pytest_cache/ | |
166 | +cover/ | |
167 | + | |
168 | +# Translations | |
169 | +*.mo | |
170 | +*.pot | |
171 | + | |
172 | +# Django stuff: | |
173 | +*.log | |
174 | +local_settings.py | |
175 | +db.sqlite3 | |
176 | +db.sqlite3-journal | |
177 | + | |
178 | +# Flask stuff: | |
179 | +instance/ | |
180 | +.webassets-cache | |
181 | + | |
182 | +# Scrapy stuff: | |
183 | +.scrapy | |
35 | 184 | |
36 | -*.directory | |
185 | +# Sphinx documentation | |
186 | +docs/_build/ | |
37 | 187 | |
188 | +# PyBuilder | |
189 | +.pybuilder/ | |
190 | +target/ | |
38 | 191 | |
39 | -# latex | |
192 | +# Jupyter Notebook | |
193 | +.ipynb_checkpoints | |
194 | + | |
195 | +# IPython | |
196 | +profile_default/ | |
197 | +ipython_config.py | |
198 | + | |
199 | +# pyenv | |
200 | +# For a library or package, you might want to ignore these files since the code is | |
201 | +# intended to run in multiple environments; otherwise, check them in: | |
202 | +# .python-version | |
203 | + | |
204 | +# pipenv | |
205 | +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. | |
206 | +# However, in case of collaboration, if having platform-specific dependencies or dependencies | |
207 | +# having no cross-platform support, pipenv may install dependencies that don't work, or not | |
208 | +# install all needed dependencies. | |
209 | +#Pipfile.lock | |
210 | + | |
211 | +# PEP 582; used by e.g. github.com/David-OConnor/pyflow | |
212 | +__pypackages__/ | |
213 | + | |
214 | +# Celery stuff | |
215 | +celerybeat-schedule | |
216 | +celerybeat.pid | |
217 | + | |
218 | +# SageMath parsed files | |
219 | +*.sage.py | |
220 | + | |
221 | +# Environments | |
222 | +.env | |
223 | +.venv | |
224 | +env/ | |
225 | +venv/ | |
226 | +ENV/ | |
227 | +env.bak/ | |
228 | +venv.bak/ | |
229 | +myenv/ | |
230 | + | |
231 | +# Spyder project settings | |
232 | +.spyderproject | |
233 | +.spyproject | |
234 | + | |
235 | +# Rope project settings | |
236 | +.ropeproject | |
237 | + | |
238 | +# mkdocs documentation | |
239 | +/site | |
240 | + | |
241 | +# mypy | |
242 | +.mypy_cache/ | |
243 | +.dmypy.json | |
244 | +dmypy.json | |
245 | + | |
246 | +# Pyre type checker | |
247 | +.pyre/ | |
248 | + | |
249 | +# pytype static type analyzer | |
250 | +.pytype/ | |
251 | + | |
252 | +# Cython debug symbols | |
253 | +cython_debug/ | |
254 | + | |
255 | +### Vim ### | |
256 | +# Swap | |
257 | +[._]*.s[a-v][a-z] | |
258 | +!*.svg # comment out if you don't need vector files | |
259 | +[._]*.sw[a-p] | |
260 | +[._]s[a-rt-v][a-z] | |
261 | +[._]ss[a-gi-z] | |
262 | +[._]sw[a-p] | |
263 | + | |
264 | +# Session | |
265 | +Session.vim | |
266 | +Sessionx.vim | |
267 | + | |
268 | +# Temporary | |
269 | +.netrwhist | |
270 | +*~ | |
271 | +# Auto-generated tag files | |
272 | +tags | |
273 | +# Persistent undo | |
274 | +[._]*.un~ | |
275 | + | |
276 | +### VisualStudioCode ### | |
277 | +.vscode/* | |
278 | +*.code-workspace | |
279 | + | |
280 | +# Local History for Visual Studio Code | |
281 | +.history/ | |
282 | + | |
283 | +### VisualStudioCode Patch ### | |
284 | +# Ignore all local history of files | |
285 | +.history | |
286 | +.ionide | |
287 | + | |
288 | +### LaTeX ### | |
289 | +## Core latex/pdflatex auxiliary files: | |
40 | 290 | *.aux |
291 | +*.lof | |
292 | +*.log | |
293 | +*.lot | |
294 | +*.fls | |
295 | +*.out | |
41 | 296 | *.toc |
297 | +*.fmt | |
298 | +*.fot | |
299 | +*.cb | |
300 | +*.cb2 | |
301 | +.*.lb | |
302 | + | |
303 | +## Intermediate documents: | |
304 | +*.dvi | |
305 | +*.xdv | |
306 | +*-converted-to.* | |
307 | +# these rules might exclude image files for figures etc. | |
308 | +# *.ps | |
309 | +# *.eps | |
310 | ||
311 | + | |
312 | +## Generated if empty string is given at "Please type another file name for output:" | |
313 | ||
314 | + | |
315 | +## Bibliography auxiliary files (bibtex/biblatex/biber): | |
42 | 316 | *.bbl |
317 | +*.bcf | |
43 | 318 | *.blg |
44 | -*.fls | |
319 | +*-blx.aux | |
320 | +*-blx.bib | |
321 | +*.run.xml | |
322 | + | |
323 | +## Build tool auxiliary files: | |
45 | 324 | *.fdb_latexmk |
325 | +*.synctex | |
326 | +*.synctex(busy) | |
46 | 327 | *.synctex.gz |
47 | -*.out | |
48 | -*.log | |
328 | +*.synctex.gz(busy) | |
329 | +*.pdfsync | |
330 | + | |
331 | +# End of https://www.toptal.com/developers/gitignore/api/vim,python,intellij,visualstudiocode,direnv | |
332 | + | |
333 | +### Cython generated files ### | |
334 | +*.c | |
335 | +*.cpp | |
336 | + | |
337 | +### Patches and diffs ### | |
338 | +*.patch | |
339 | +*.diff | ... | ... |
invesalius/constants.py
... | ... | @@ -761,17 +761,38 @@ OBJA = wx.NewId() |
761 | 761 | OBJC = wx.NewId() |
762 | 762 | OBJF = wx.NewId() |
763 | 763 | |
764 | -BTNS_OBJ = {OBJL: {0: _('Left')}, | |
765 | - OBJR: {1: _('Right')}, | |
766 | - OBJA: {2: _('Anterior')}, | |
767 | - OBJC: {3: _('Center')}, | |
768 | - OBJF: {4: _('Fixed')}} | |
769 | - | |
770 | -TIPS_OBJ = [_("Select left object fiducial"), | |
771 | - _("Select right object fiducial"), | |
772 | - _("Select anterior object fiducial"), | |
773 | - _("Select object center"), | |
774 | - _("Attach sensor to object")] | |
764 | +OBJECT_FIDUCIALS = [ | |
765 | + { | |
766 | + 'fiducial_index': 0, | |
767 | + 'button_id': OBJL, | |
768 | + 'label': _('Left'), | |
769 | + 'tip': _("Select left object fiducial"), | |
770 | + }, | |
771 | + { | |
772 | + 'fiducial_index': 1, | |
773 | + 'button_id': OBJR, | |
774 | + 'label': _('Right'), | |
775 | + 'tip': _("Select right object fiducial"), | |
776 | + }, | |
777 | + { | |
778 | + 'fiducial_index': 2, | |
779 | + 'button_id': OBJA, | |
780 | + 'label': _('Anterior'), | |
781 | + 'tip': _("Select anterior object fiducial"), | |
782 | + }, | |
783 | + { | |
784 | + 'fiducial_index': 3, | |
785 | + 'button_id': OBJC, | |
786 | + 'label': _('Center'), | |
787 | + 'tip': _("Select object center"), | |
788 | + }, | |
789 | + { | |
790 | + 'fiducial_index': 4, | |
791 | + 'button_id': OBJF, | |
792 | + 'label': _('Fixed'), | |
793 | + 'tip': _("Attach sensor to object"), | |
794 | + }, | |
795 | +] | |
775 | 796 | |
776 | 797 | MTC_PROBE_NAME = "1Probe" |
777 | 798 | MTC_REF_NAME = "2Ref" | ... | ... |
invesalius/data/bases.py
... | ... | @@ -188,6 +188,13 @@ def object_registration(fiducials, orients, coord_raw, m_change): |
188 | 188 | fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3] |
189 | 189 | |
190 | 190 | # compute initial alignment of probe fixed in the object in source frame |
191 | + | |
192 | + # XXX: Some duplicate processing is done here: the Euler angles are calculated once by | |
193 | + # the lines below, and then again in dco.coordinates_to_transformation_matrix. | |
194 | + # | |
195 | + a, b, g = np.radians(coords[3, 3:]) | |
196 | + r_s0_raw = tr.euler_matrix(a, b, g, axes='rzyx') | |
197 | + | |
191 | 198 | s0_raw = dco.coordinates_to_transformation_matrix( |
192 | 199 | position=coords[3, :3], |
193 | 200 | orientation=coords[3, 3:], | ... | ... |
invesalius/data/converters.py
... | ... | @@ -149,6 +149,7 @@ def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)): |
149 | 149 | # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html |
150 | 150 | def gdcm_to_numpy(image, apply_intercep_scale=True): |
151 | 151 | map_gdcm_np = { |
152 | + gdcm.PixelFormat.SINGLEBIT: np.uint8, | |
152 | 153 | gdcm.PixelFormat.UINT8: np.uint8, |
153 | 154 | gdcm.PixelFormat.INT8: np.int8, |
154 | 155 | gdcm.PixelFormat.UINT12: np.uint16, |
... | ... | @@ -177,6 +178,8 @@ def gdcm_to_numpy(image, apply_intercep_scale=True): |
177 | 178 | np_array = np.frombuffer( |
178 | 179 | gdcm_array.encode("utf-8", errors="surrogateescape"), dtype=dtype |
179 | 180 | ) |
181 | + if pf.GetScalarType() == gdcm.PixelFormat.SINGLEBIT: | |
182 | + np_array = np.unpackbits(np_array) | |
180 | 183 | np_array.shape = shape |
181 | 184 | np_array = np_array.squeeze() |
182 | 185 | ... | ... |
invesalius/gui/dialogs.py
... | ... | @@ -3304,8 +3304,9 @@ class MaskDensityDialog(wx.Dialog): |
3304 | 3304 | |
3305 | 3305 | class ObjectCalibrationDialog(wx.Dialog): |
3306 | 3306 | |
3307 | - def __init__(self, tracker): | |
3307 | + def __init__(self, tracker, pedal_connection): | |
3308 | 3308 | self.tracker = tracker |
3309 | + self.pedal_connection = pedal_connection | |
3309 | 3310 | |
3310 | 3311 | self.trk_init, self.tracker_id = tracker.GetTrackerInfo() |
3311 | 3312 | |
... | ... | @@ -3313,6 +3314,7 @@ class ObjectCalibrationDialog(wx.Dialog): |
3313 | 3314 | self.obj_name = None |
3314 | 3315 | self.polydata = None |
3315 | 3316 | self.use_default_object = False |
3317 | + self.object_fiducial_being_set = None | |
3316 | 3318 | |
3317 | 3319 | self.obj_fiducials = np.full([5, 3], np.nan) |
3318 | 3320 | self.obj_orients = np.full([5, 3], np.nan) |
... | ... | @@ -3323,6 +3325,11 @@ class ObjectCalibrationDialog(wx.Dialog): |
3323 | 3325 | self._init_gui() |
3324 | 3326 | self.LoadObject() |
3325 | 3327 | |
3328 | + self.__bind_events() | |
3329 | + | |
3330 | + def __bind_events(self): | |
3331 | + Publisher.subscribe(self.SetObjectFiducial, 'Set object fiducial') | |
3332 | + | |
3326 | 3333 | def _init_gui(self): |
3327 | 3334 | self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) |
3328 | 3335 | self.interactor.Enable(1) |
... | ... | @@ -3373,15 +3380,17 @@ class ObjectCalibrationDialog(wx.Dialog): |
3373 | 3380 | choice_sensor]) |
3374 | 3381 | |
3375 | 3382 | # Push buttons for object fiducials |
3376 | - btns_obj = const.BTNS_OBJ | |
3377 | - tips_obj = const.TIPS_OBJ | |
3383 | + for object_fiducial in const.OBJECT_FIDUCIALS: | |
3384 | + index = object_fiducial['fiducial_index'] | |
3385 | + label = object_fiducial['label'] | |
3386 | + button_id = object_fiducial['button_id'] | |
3387 | + tip = object_fiducial['tip'] | |
3378 | 3388 | |
3379 | - for k in btns_obj: | |
3380 | - n = list(btns_obj[k].keys())[0] | |
3381 | - lab = list(btns_obj[k].values())[0] | |
3382 | - self.btns_coord[n] = wx.Button(self, k, label=lab, size=wx.Size(60, 23)) | |
3383 | - self.btns_coord[n].SetToolTip(wx.ToolTip(tips_obj[n])) | |
3384 | - self.btns_coord[n].Bind(wx.EVT_BUTTON, self.OnGetObjectFiducials) | |
3389 | + ctrl = wx.ToggleButton(self, button_id, label=label, size=wx.Size(60, 23)) | |
3390 | + ctrl.SetToolTip(wx.ToolTip(tip)) | |
3391 | + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnObjectFiducialButton, index, ctrl=ctrl)) | |
3392 | + | |
3393 | + self.btns_coord[index] = ctrl | |
3385 | 3394 | |
3386 | 3395 | for m in range(0, 5): |
3387 | 3396 | for n in range(0, 3): |
... | ... | @@ -3525,13 +3534,42 @@ class ObjectCalibrationDialog(wx.Dialog): |
3525 | 3534 | self.ren.AddActor(ball_actor) |
3526 | 3535 | return ball_actor, tactor |
3527 | 3536 | |
3528 | - def OnGetObjectFiducials(self, evt): | |
3537 | + def OnObjectFiducialButton(self, index, evt, ctrl): | |
3529 | 3538 | if not self.tracker.IsTrackerInitialized(): |
3530 | 3539 | ShowNavigationTrackerWarning(0, 'choose') |
3531 | 3540 | return |
3532 | 3541 | |
3533 | - btn_id = list(const.BTNS_OBJ[evt.GetId()].keys())[0] | |
3542 | + # TODO: The code below until the end of the function is essentially copy-paste from | |
3543 | + # OnTrackerFiducials function in NeuronavigationPanel class. Probably the easiest | |
3544 | + # way to deduplicate this would be to create a Fiducial class, which would contain | |
3545 | + # this code just once. | |
3546 | + # | |
3547 | + | |
3548 | + # Do not allow several object fiducials to be set at the same time. | |
3549 | + if self.object_fiducial_being_set is not None and self.object_fiducial_being_set != index: | |
3550 | + ctrl.SetValue(False) | |
3551 | + return | |
3552 | + | |
3553 | + # Called when the button for setting the object fiducial is enabled and either pedal is pressed | |
3554 | + # or the button is pressed again. | |
3555 | + # | |
3556 | + def set_fiducial_callback(): | |
3557 | + Publisher.sendMessage('Set object fiducial', fiducial_index=index) | |
3558 | + if self.pedal_connection is not None: | |
3559 | + self.pedal_connection.remove_callback('fiducial') | |
3560 | + | |
3561 | + ctrl.SetValue(False) | |
3562 | + self.object_fiducial_being_set = None | |
3563 | + | |
3564 | + if ctrl.GetValue(): | |
3565 | + self.object_fiducial_being_set = index | |
3566 | + | |
3567 | + if self.pedal_connection is not None: | |
3568 | + self.pedal_connection.add_callback('fiducial', set_fiducial_callback) | |
3569 | + else: | |
3570 | + set_fiducial_callback() | |
3534 | 3571 | |
3572 | + def SetObjectFiducial(self, fiducial_index): | |
3535 | 3573 | coord, coord_raw = self.tracker.GetTrackerCoordinates( |
3536 | 3574 | # XXX: Always use static reference mode when getting the coordinates. This is what the |
3537 | 3575 | # code did previously, as well. At some point, it should probably be thought through |
... | ... | @@ -3549,22 +3587,22 @@ class ObjectCalibrationDialog(wx.Dialog): |
3549 | 3587 | # mode" principle above, but it's hard to come up with a simple change to increase the consistency |
3550 | 3588 | # and not change the function to the point of potentially breaking it.) |
3551 | 3589 | # |
3552 | - if self.obj_ref_id and btn_id == 4: | |
3590 | + if self.obj_ref_id and fiducial_index == 4: | |
3553 | 3591 | coord = coord_raw[self.obj_ref_id, :] |
3554 | 3592 | coord[2] = -coord[2] |
3555 | 3593 | |
3556 | - if btn_id == 3: | |
3594 | + if fiducial_index == 3: | |
3557 | 3595 | coord = np.zeros([6,]) |
3558 | 3596 | |
3559 | 3597 | # Update text controls with tracker coordinates |
3560 | 3598 | if coord is not None or np.sum(coord) != 0.0: |
3561 | - self.obj_fiducials[btn_id, :] = coord[:3] | |
3562 | - self.obj_orients[btn_id, :] = coord[3:] | |
3563 | - for n in [0, 1, 2]: | |
3564 | - self.txt_coord[btn_id][n].SetLabel(str(round(coord[n], 1))) | |
3565 | - if self.text_actors[btn_id]: | |
3566 | - self.text_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0) | |
3567 | - self.ball_actors[btn_id].GetProperty().SetColor(0.0, 1.0, 0.0) | |
3599 | + self.obj_fiducials[fiducial_index, :] = coord[:3] | |
3600 | + self.obj_orients[fiducial_index, :] = coord[3:] | |
3601 | + for i in [0, 1, 2]: | |
3602 | + self.txt_coord[fiducial_index][i].SetLabel(str(round(coord[i], 1))) | |
3603 | + if self.text_actors[fiducial_index]: | |
3604 | + self.text_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0) | |
3605 | + self.ball_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0) | |
3568 | 3606 | self.Refresh() |
3569 | 3607 | else: |
3570 | 3608 | ShowNavigationTrackerWarning(0, 'choose') | ... | ... |
invesalius/gui/task_navigator.py
... | ... | @@ -18,12 +18,9 @@ |
18 | 18 | #-------------------------------------------------------------------------- |
19 | 19 | |
20 | 20 | from functools import partial |
21 | +import itertools | |
21 | 22 | import csv |
22 | -import os | |
23 | -import queue | |
24 | -import sys | |
25 | 23 | import time |
26 | -import threading | |
27 | 24 | |
28 | 25 | import nibabel as nb |
29 | 26 | import numpy as np |
... | ... | @@ -35,10 +32,8 @@ except ImportError: |
35 | 32 | import wx |
36 | 33 | |
37 | 34 | try: |
38 | - import wx.lib.agw.hyperlink as hl | |
39 | 35 | import wx.lib.agw.foldpanelbar as fpb |
40 | 36 | except ImportError: |
41 | - import wx.lib.hyperlink as hl | |
42 | 37 | import wx.lib.foldpanelbar as fpb |
43 | 38 | |
44 | 39 | import wx.lib.colourselect as csel |
... | ... | @@ -47,25 +42,22 @@ from invesalius.pubsub import pub as Publisher |
47 | 42 | from time import sleep |
48 | 43 | |
49 | 44 | import invesalius.constants as const |
50 | -import invesalius.data.bases as db | |
51 | 45 | |
52 | 46 | if has_trekker: |
53 | 47 | import invesalius.data.brainmesh_handler as brain |
54 | 48 | |
55 | -import invesalius.data.coordinates as dco | |
56 | -import invesalius.data.coregistration as dcr | |
57 | 49 | import invesalius.data.imagedata_utils as imagedata_utils |
58 | -import invesalius.data.serial_port_connection as spc | |
59 | 50 | import invesalius.data.slice_ as sl |
60 | -import invesalius.data.trackers as dt | |
61 | 51 | import invesalius.data.tractography as dti |
62 | -import invesalius.data.transformations as tr | |
63 | 52 | import invesalius.data.record_coords as rec |
64 | 53 | import invesalius.data.vtk_utils as vtk_utils |
65 | 54 | import invesalius.gui.dialogs as dlg |
66 | 55 | import invesalius.project as prj |
67 | 56 | from invesalius import utils |
68 | 57 | from invesalius.gui import utils as gui_utils |
58 | +from invesalius.navigation.icp import ICP | |
59 | +from invesalius.navigation.navigation import Navigation | |
60 | +from invesalius.navigation.tracker import Tracker | |
69 | 61 | |
70 | 62 | HAS_PEDAL_CONNECTION = True |
71 | 63 | try: |
... | ... | @@ -161,9 +153,10 @@ class InnerFoldPanel(wx.Panel): |
161 | 153 | fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, |
162 | 154 | (10, 310), 0, fpb.FPB_SINGLE_FOLD) |
163 | 155 | |
164 | - # Initialize Tracker object here so that it is available to several panels. | |
156 | + # Initialize Tracker and PedalConnection objects here so that they are available to several panels. | |
165 | 157 | # |
166 | 158 | tracker = Tracker() |
159 | + pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None | |
167 | 160 | |
168 | 161 | # Fold panel style |
169 | 162 | style = fpb.CaptionBarStyle() |
... | ... | @@ -173,7 +166,7 @@ class InnerFoldPanel(wx.Panel): |
173 | 166 | |
174 | 167 | # Fold 1 - Navigation panel |
175 | 168 | item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) |
176 | - ntw = NeuronavigationPanel(item, tracker) | |
169 | + ntw = NeuronavigationPanel(item, tracker, pedal_connection) | |
177 | 170 | |
178 | 171 | fold_panel.ApplyCaptionStyle(item, style) |
179 | 172 | fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, |
... | ... | @@ -182,7 +175,7 @@ class InnerFoldPanel(wx.Panel): |
182 | 175 | |
183 | 176 | # Fold 2 - Object registration panel |
184 | 177 | item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) |
185 | - otw = ObjectRegistrationPanel(item, tracker) | |
178 | + otw = ObjectRegistrationPanel(item, tracker, pedal_connection) | |
186 | 179 | |
187 | 180 | fold_panel.ApplyCaptionStyle(item, style) |
188 | 181 | fold_panel.AddFoldPanelWindow(item, otw, spacing=0, |
... | ... | @@ -315,390 +308,8 @@ class InnerFoldPanel(wx.Panel): |
315 | 308 | Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue()) |
316 | 309 | |
317 | 310 | |
318 | -class Navigation(): | |
319 | - def __init__(self, pedal_connection): | |
320 | - self.pedal_connection = pedal_connection | |
321 | - | |
322 | - self.image_fiducials = np.full([3, 3], np.nan) | |
323 | - self.correg = None | |
324 | - self.current_coord = 0, 0, 0 | |
325 | - self.target = None | |
326 | - self.obj_reg = None | |
327 | - self.track_obj = False | |
328 | - self.m_change = None | |
329 | - self.all_fiducials = np.zeros((6, 6)) | |
330 | - | |
331 | - self.event = threading.Event() | |
332 | - self.coord_queue = QueueCustom(maxsize=1) | |
333 | - self.icp_queue = QueueCustom(maxsize=1) | |
334 | - # self.visualization_queue = QueueCustom(maxsize=1) | |
335 | - self.serial_port_queue = QueueCustom(maxsize=1) | |
336 | - self.coord_tracts_queue = QueueCustom(maxsize=1) | |
337 | - self.tracts_queue = QueueCustom(maxsize=1) | |
338 | - | |
339 | - # Tracker parameters | |
340 | - self.ref_mode_id = const.DEFAULT_REF_MODE | |
341 | - | |
342 | - # Tractography parameters | |
343 | - self.trk_inp = None | |
344 | - self.trekker = None | |
345 | - self.n_threads = None | |
346 | - self.view_tracts = False | |
347 | - self.peel_loaded = False | |
348 | - self.enable_act = False | |
349 | - self.act_data = None | |
350 | - self.n_tracts = const.N_TRACTS | |
351 | - self.seed_offset = const.SEED_OFFSET | |
352 | - self.seed_radius = const.SEED_RADIUS | |
353 | - self.sleep_nav = const.SLEEP_NAVIGATION | |
354 | - | |
355 | - # Serial port | |
356 | - self.serial_port = None | |
357 | - self.serial_port_connection = None | |
358 | - | |
359 | - # During navigation | |
360 | - self.coil_at_target = False | |
361 | - | |
362 | - self.__bind_events() | |
363 | - | |
364 | - def __bind_events(self): | |
365 | - Publisher.subscribe(self.CoilAtTarget, 'Coil at target') | |
366 | - | |
367 | - def CoilAtTarget(self, state): | |
368 | - self.coil_at_target = state | |
369 | - | |
370 | - def UpdateSleep(self, sleep): | |
371 | - self.sleep_nav = sleep | |
372 | - self.serial_port_connection.sleep_nav = sleep | |
373 | - | |
374 | - def SerialPortEnabled(self): | |
375 | - return self.serial_port is not None | |
376 | - | |
377 | - def SetReferenceMode(self, value): | |
378 | - self.ref_mode_id = value | |
379 | - | |
380 | - def GetReferenceMode(self): | |
381 | - return self.ref_mode_id | |
382 | - | |
383 | - def SetImageFiducial(self, fiducial_index, coord): | |
384 | - self.image_fiducials[fiducial_index, :] = coord | |
385 | - | |
386 | - print("Set image fiducial {} to coordinates {}".format(fiducial_index, coord)) | |
387 | - | |
388 | - def AreImageFiducialsSet(self): | |
389 | - return not np.isnan(self.image_fiducials).any() | |
390 | - | |
391 | - def UpdateFiducialRegistrationError(self, tracker): | |
392 | - tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials() | |
393 | - | |
394 | - self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials]) | |
395 | - | |
396 | - self.fre = db.calculate_fre(tracker_fiducials_raw, self.all_fiducials, self.ref_mode_id, self.m_change) | |
397 | - | |
398 | - def GetFiducialRegistrationError(self, icp): | |
399 | - fre = icp.icp_fre if icp.use_icp else self.fre | |
400 | - return fre, fre <= const.FIDUCIAL_REGISTRATION_ERROR_THRESHOLD | |
401 | - | |
402 | - def PedalStateChanged(self, state): | |
403 | - if state is True and self.coil_at_target and self.SerialPortEnabled(): | |
404 | - self.serial_port_connection.SendPulse() | |
405 | - | |
406 | - def StartNavigation(self, tracker): | |
407 | - tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials() | |
408 | - | |
409 | - # initialize jobs list | |
410 | - jobs_list = [] | |
411 | - | |
412 | - if self.event.is_set(): | |
413 | - self.event.clear() | |
414 | - | |
415 | - vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded] | |
416 | - vis_queues = [self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue] | |
417 | - | |
418 | - Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components) | |
419 | - | |
420 | - self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials]) | |
421 | - | |
422 | - # fiducials matrix | |
423 | - m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T, | |
424 | - shear=False, scale=False) | |
425 | - self.m_change = m_change | |
426 | - | |
427 | - errors = False | |
428 | - | |
429 | - if self.track_obj: | |
430 | - # if object tracking is selected | |
431 | - if self.obj_reg is None: | |
432 | - # check if object registration was performed | |
433 | - wx.MessageBox(_("Perform coil registration before navigation."), _("InVesalius 3")) | |
434 | - errors = True | |
435 | - else: | |
436 | - # if object registration was correctly performed continue with navigation | |
437 | - # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix | |
438 | - obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg | |
439 | - | |
440 | - coreg_data = [m_change, obj_ref_mode] | |
441 | - | |
442 | - if self.ref_mode_id: | |
443 | - coord_raw = dco.GetCoordinates(tracker.trk_init, tracker.tracker_id, self.ref_mode_id) | |
444 | - else: | |
445 | - coord_raw = np.array([None]) | |
446 | - | |
447 | - obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change) | |
448 | - coreg_data.extend(obj_data) | |
449 | - | |
450 | - queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue] | |
451 | - jobs_list.append(dcr.CoordinateCorregistrate(self.ref_mode_id, tracker, coreg_data, | |
452 | - self.view_tracts, queues, | |
453 | - self.event, self.sleep_nav, tracker.tracker_id, | |
454 | - self.target)) | |
455 | - else: | |
456 | - coreg_data = (m_change, 0) | |
457 | - queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue] | |
458 | - jobs_list.append(dcr.CoordinateCorregistrateNoObject(self.ref_mode_id, tracker, coreg_data, | |
459 | - self.view_tracts, queues, | |
460 | - self.event, self.sleep_nav)) | |
461 | - | |
462 | - if not errors: | |
463 | - #TODO: Test the serial port thread | |
464 | - if self.SerialPortEnabled(): | |
465 | - self.serial_port_connection = spc.SerialPortConnection( | |
466 | - self.serial_port, | |
467 | - self.serial_port_queue, | |
468 | - self.event, | |
469 | - self.sleep_nav, | |
470 | - ) | |
471 | - self.serial_port_connection.Connect() | |
472 | - jobs_list.append(self.serial_port_connection) | |
473 | - | |
474 | - if self.view_tracts: | |
475 | - # initialize Trekker parameters | |
476 | - slic = sl.Slice() | |
477 | - prj_data = prj.Project() | |
478 | - matrix_shape = tuple(prj_data.matrix_shape) | |
479 | - affine = slic.affine.copy() | |
480 | - affine[1, -1] -= matrix_shape[1] | |
481 | - affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) | |
482 | - Publisher.sendMessage("Update marker offset state", create=True) | |
483 | - self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius,\ | |
484 | - self.n_threads, self.act_data, affine_vtk, matrix_shape[1] | |
485 | - # print("Appending the tract computation thread!") | |
486 | - queues = [self.coord_tracts_queue, self.tracts_queue] | |
487 | - if self.enable_act: | |
488 | - jobs_list.append(dti.ComputeTractsACTThread(self.trk_inp, queues, self.event, self.sleep_nav)) | |
489 | - else: | |
490 | - jobs_list.append(dti.ComputeTractsThread(self.trk_inp, queues, self.event, self.sleep_nav)) | |
491 | - | |
492 | - jobs_list.append(UpdateNavigationScene(vis_queues, vis_components, | |
493 | - self.event, self.sleep_nav)) | |
494 | - | |
495 | - for jobs in jobs_list: | |
496 | - # jobs.daemon = True | |
497 | - jobs.start() | |
498 | - # del jobs | |
499 | - | |
500 | - if self.pedal_connection is not None: | |
501 | - self.pedal_connection.add_callback('navigation', self.PedalStateChanged) | |
502 | - | |
503 | - def StopNavigation(self): | |
504 | - self.event.set() | |
505 | - | |
506 | - if self.pedal_connection is not None: | |
507 | - self.pedal_connection.remove_callback('navigation') | |
508 | - | |
509 | - self.coord_queue.clear() | |
510 | - self.coord_queue.join() | |
511 | - | |
512 | - if self.SerialPortEnabled(): | |
513 | - self.serial_port_connection.join() | |
514 | - | |
515 | - self.serial_port_queue.clear() | |
516 | - self.serial_port_queue.join() | |
517 | - | |
518 | - if self.view_tracts: | |
519 | - self.coord_tracts_queue.clear() | |
520 | - self.coord_tracts_queue.join() | |
521 | - | |
522 | - self.tracts_queue.clear() | |
523 | - self.tracts_queue.join() | |
524 | - | |
525 | - vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded] | |
526 | - Publisher.sendMessage("Navigation status", nav_status=False, vis_status=vis_components) | |
527 | - | |
528 | - | |
529 | -class Tracker(): | |
530 | - def __init__(self): | |
531 | - self.trk_init = None | |
532 | - self.tracker_id = const.DEFAULT_TRACKER | |
533 | - | |
534 | - self.tracker_fiducials = np.full([3, 3], np.nan) | |
535 | - self.tracker_fiducials_raw = np.zeros((6, 6)) | |
536 | - | |
537 | - self.tracker_connected = False | |
538 | - | |
539 | - def SetTracker(self, new_tracker): | |
540 | - if new_tracker: | |
541 | - self.DisconnectTracker() | |
542 | - | |
543 | - self.trk_init = dt.TrackerConnection(new_tracker, None, 'connect') | |
544 | - if not self.trk_init[0]: | |
545 | - dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1]) | |
546 | - | |
547 | - self.tracker_id = 0 | |
548 | - self.tracker_connected = False | |
549 | - else: | |
550 | - self.tracker_id = new_tracker | |
551 | - self.tracker_connected = True | |
552 | - | |
553 | - def DisconnectTracker(self): | |
554 | - if self.tracker_connected: | |
555 | - self.ResetTrackerFiducials() | |
556 | - Publisher.sendMessage('Update status text in GUI', | |
557 | - label=_("Disconnecting tracker ...")) | |
558 | - Publisher.sendMessage('Remove sensors ID') | |
559 | - Publisher.sendMessage('Remove object data') | |
560 | - self.trk_init = dt.TrackerConnection(self.tracker_id, self.trk_init[0], 'disconnect') | |
561 | - if not self.trk_init[0]: | |
562 | - self.tracker_connected = False | |
563 | - self.tracker_id = 0 | |
564 | - | |
565 | - Publisher.sendMessage('Update status text in GUI', | |
566 | - label=_("Tracker disconnected")) | |
567 | - print("Tracker disconnected!") | |
568 | - else: | |
569 | - Publisher.sendMessage('Update status text in GUI', | |
570 | - label=_("Tracker still connected")) | |
571 | - print("Tracker still connected!") | |
572 | - | |
573 | - def IsTrackerInitialized(self): | |
574 | - return self.trk_init and self.tracker_id and self.tracker_connected | |
575 | - | |
576 | - def AreTrackerFiducialsSet(self): | |
577 | - return not np.isnan(self.tracker_fiducials).any() | |
578 | - | |
579 | - def GetTrackerCoordinates(self, ref_mode_id, n_samples=1): | |
580 | - coord_raw_samples = {} | |
581 | - coord_samples = {} | |
582 | - | |
583 | - for i in range(n_samples): | |
584 | - coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, ref_mode_id) | |
585 | - | |
586 | - if ref_mode_id == const.DYNAMIC_REF: | |
587 | - coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :]) | |
588 | - else: | |
589 | - coord = coord_raw[0, :] | |
590 | - coord[2] = -coord[2] | |
591 | - | |
592 | - coord_raw_samples[i] = coord_raw | |
593 | - coord_samples[i] = coord | |
594 | - | |
595 | - coord_raw_avg = np.median(list(coord_raw_samples.values()), axis=0) | |
596 | - coord_avg = np.median(list(coord_samples.values()), axis=0) | |
597 | - | |
598 | - return coord_avg, coord_raw_avg | |
599 | - | |
600 | - def SetTrackerFiducial(self, ref_mode_id, fiducial_index): | |
601 | - coord, coord_raw = self.GetTrackerCoordinates( | |
602 | - ref_mode_id=ref_mode_id, | |
603 | - n_samples=const.CALIBRATION_TRACKER_SAMPLES, | |
604 | - ) | |
605 | - | |
606 | - # Update tracker fiducial with tracker coordinates | |
607 | - self.tracker_fiducials[fiducial_index, :] = coord[0:3] | |
608 | - | |
609 | - assert 0 <= fiducial_index <= 2, "Fiducial index out of range (0-2): {}".format(fiducial_index) | |
610 | - | |
611 | - self.tracker_fiducials_raw[2 * fiducial_index, :] = coord_raw[0, :] | |
612 | - self.tracker_fiducials_raw[2 * fiducial_index + 1, :] = coord_raw[1, :] | |
613 | - | |
614 | - print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3])) | |
615 | - | |
616 | - def ResetTrackerFiducials(self): | |
617 | - for m in range(3): | |
618 | - self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan] | |
619 | - | |
620 | - def GetTrackerFiducials(self): | |
621 | - return self.tracker_fiducials, self.tracker_fiducials_raw | |
622 | - | |
623 | - def GetTrackerInfo(self): | |
624 | - return self.trk_init, self.tracker_id | |
625 | - | |
626 | - def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre): | |
627 | - if self.tracker_connected: | |
628 | - selection_ctrl.SetSelection(self.tracker_id) | |
629 | - else: | |
630 | - selection_ctrl.SetSelection(0) | |
631 | - | |
632 | - # Update tracker location in the UI. | |
633 | - for m in range(3): | |
634 | - coord = self.tracker_fiducials[m, :] | |
635 | - for n in range(0, 3): | |
636 | - value = 0.0 if np.isnan(coord[n]) else float(coord[n]) | |
637 | - numctrls_fiducial[m][n].SetValue(value) | |
638 | - | |
639 | - txtctrl_fre.SetValue('') | |
640 | - txtctrl_fre.SetBackgroundColour('WHITE') | |
641 | - | |
642 | - def get_trackers(self): | |
643 | - return const.TRACKERS | |
644 | - | |
645 | -class ICP(): | |
646 | - def __init__(self): | |
647 | - self.use_icp = False | |
648 | - self.m_icp = None | |
649 | - self.icp_fre = None | |
650 | - | |
651 | - def StartICP(self, navigation, tracker): | |
652 | - ref_mode_id = navigation.GetReferenceMode() | |
653 | - | |
654 | - if not self.use_icp: | |
655 | - if dlg.ICPcorregistration(navigation.fre): | |
656 | - Publisher.sendMessage('Stop navigation') | |
657 | - use_icp, self.m_icp = self.OnICP(navigation, tracker, navigation.m_change) | |
658 | - if use_icp: | |
659 | - self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials, | |
660 | - ref_mode_id, navigation.m_change, self.m_icp) | |
661 | - self.SetICP(navigation, use_icp) | |
662 | - else: | |
663 | - print("ICP canceled") | |
664 | - Publisher.sendMessage('Start navigation') | |
665 | - | |
666 | - def OnICP(self, navigation, tracker, m_change): | |
667 | - ref_mode_id = navigation.GetReferenceMode() | |
668 | - | |
669 | - dialog = dlg.ICPCorregistrationDialog(nav_prop=(m_change, tracker.tracker_id, tracker.trk_init, ref_mode_id)) | |
670 | - | |
671 | - if dialog.ShowModal() == wx.ID_OK: | |
672 | - m_icp, point_coord, transformed_points, prev_error, final_error = dialog.GetValue() | |
673 | - # TODO: checkbox in the dialog to transfer the icp points to 3D viewer | |
674 | - #create markers | |
675 | - # for i in range(len(point_coord)): | |
676 | - # img_coord = point_coord[i][0],-point_coord[i][1],point_coord[i][2], 0, 0, 0 | |
677 | - # transf_coord = transformed_points[i][0],-transformed_points[i][1],transformed_points[i][2], 0, 0, 0 | |
678 | - # Publisher.sendMessage('Create marker', coord=img_coord, marker_id=None, colour=(1,0,0)) | |
679 | - # Publisher.sendMessage('Create marker', coord=transf_coord, marker_id=None, colour=(0,0,1)) | |
680 | - if m_icp is not None: | |
681 | - dlg.ReportICPerror(prev_error, final_error) | |
682 | - use_icp = True | |
683 | - else: | |
684 | - use_icp = False | |
685 | - | |
686 | - return use_icp, m_icp | |
687 | - | |
688 | - else: | |
689 | - return self.use_icp, self.m_icp | |
690 | - | |
691 | - def SetICP(self, navigation, use_icp): | |
692 | - self.use_icp = use_icp | |
693 | - navigation.icp_queue.put_nowait([self.use_icp, self.m_icp]) | |
694 | - | |
695 | - def ResetICP(self): | |
696 | - self.use_icp = False | |
697 | - self.m_icp = None | |
698 | - self.icp_fre = None | |
699 | - | |
700 | 311 | class NeuronavigationPanel(wx.Panel): |
701 | - def __init__(self, parent, tracker): | |
312 | + def __init__(self, parent, tracker, pedal_connection): | |
702 | 313 | wx.Panel.__init__(self, parent) |
703 | 314 | try: |
704 | 315 | default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) |
... | ... | @@ -711,15 +322,16 @@ class NeuronavigationPanel(wx.Panel): |
711 | 322 | self.__bind_events() |
712 | 323 | |
713 | 324 | # Initialize global variables |
714 | - self.pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None | |
325 | + self.pedal_connection = pedal_connection | |
715 | 326 | self.navigation = Navigation( |
716 | - pedal_connection=self.pedal_connection, | |
327 | + pedal_connection=pedal_connection, | |
717 | 328 | ) |
718 | 329 | self.icp = ICP() |
719 | 330 | self.tracker = tracker |
720 | 331 | |
721 | 332 | self.nav_status = False |
722 | 333 | self.tracker_fiducial_being_set = None |
334 | + self.current_coord = 0, 0, 0 | |
723 | 335 | |
724 | 336 | # Initialize list of buttons and numctrls for wx objects |
725 | 337 | self.btns_set_fiducial = [None, None, None, None, None, None] |
... | ... | @@ -776,7 +388,7 @@ class NeuronavigationPanel(wx.Panel): |
776 | 388 | txt_fre = wx.StaticText(self, -1, _('FRE:')) |
777 | 389 | txt_icp = wx.StaticText(self, -1, _('Refine:')) |
778 | 390 | |
779 | - if self.pedal_connection is not None and self.pedal_connection.in_use: | |
391 | + if pedal_connection is not None and pedal_connection.in_use: | |
780 | 392 | txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) |
781 | 393 | else: |
782 | 394 | txt_pedal_pressed = None |
... | ... | @@ -805,14 +417,14 @@ class NeuronavigationPanel(wx.Panel): |
805 | 417 | self.checkbox_icp = checkbox_icp |
806 | 418 | |
807 | 419 | # An indicator for pedal trigger |
808 | - if self.pedal_connection is not None and self.pedal_connection.in_use: | |
420 | + if pedal_connection is not None and pedal_connection.in_use: | |
809 | 421 | tooltip = wx.ToolTip(_(u"Is the pedal pressed")) |
810 | 422 | checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) |
811 | 423 | checkbox_pedal_pressed.SetValue(False) |
812 | 424 | checkbox_pedal_pressed.Enable(False) |
813 | 425 | checkbox_pedal_pressed.SetToolTip(tooltip) |
814 | 426 | |
815 | - self.pedal_connection.add_callback('gui', checkbox_pedal_pressed.SetValue) | |
427 | + pedal_connection.add_callback('gui', checkbox_pedal_pressed.SetValue) | |
816 | 428 | |
817 | 429 | self.checkbox_pedal_pressed = checkbox_pedal_pressed |
818 | 430 | else: |
... | ... | @@ -846,7 +458,7 @@ class NeuronavigationPanel(wx.Panel): |
846 | 458 | (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) |
847 | 459 | |
848 | 460 | pedal_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) |
849 | - if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: | |
461 | + if HAS_PEDAL_CONNECTION and pedal_connection.in_use: | |
850 | 462 | pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), |
851 | 463 | (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) |
852 | 464 | |
... | ... | @@ -982,7 +594,7 @@ class NeuronavigationPanel(wx.Panel): |
982 | 594 | |
983 | 595 | def UpdateImageCoordinates(self, position): |
984 | 596 | # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication. |
985 | - self.navigation.current_coord = position | |
597 | + self.current_coord = position | |
986 | 598 | |
987 | 599 | for m in [0, 1, 2]: |
988 | 600 | if not self.btns_set_fiducial[m].GetValue(): |
... | ... | @@ -1039,7 +651,7 @@ class NeuronavigationPanel(wx.Panel): |
1039 | 651 | fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] |
1040 | 652 | |
1041 | 653 | # XXX: This is still a bit hard to read, could be cleaned up. |
1042 | - marker_id = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0] | |
654 | + label = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0] | |
1043 | 655 | |
1044 | 656 | if self.btns_set_fiducial[n].GetValue(): |
1045 | 657 | coord = self.numctrls_fiducial[n][0].GetValue(),\ |
... | ... | @@ -1053,13 +665,13 @@ class NeuronavigationPanel(wx.Panel): |
1053 | 665 | seed = 3 * [0.] |
1054 | 666 | |
1055 | 667 | Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size, |
1056 | - marker_id=marker_id, seed=seed) | |
668 | + marker_id=label, seed=seed) | |
1057 | 669 | else: |
1058 | 670 | for m in [0, 1, 2]: |
1059 | 671 | self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) |
1060 | 672 | |
1061 | 673 | Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=np.nan) |
1062 | - Publisher.sendMessage('Delete fiducial marker', marker_id=marker_id) | |
674 | + Publisher.sendMessage('Delete fiducial marker', label=label) | |
1063 | 675 | |
1064 | 676 | def OnTrackerFiducials(self, n, evt, ctrl): |
1065 | 677 | |
... | ... | @@ -1074,14 +686,16 @@ class NeuronavigationPanel(wx.Panel): |
1074 | 686 | def set_fiducial_callback(): |
1075 | 687 | fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name'] |
1076 | 688 | Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) |
1077 | - self.pedal_connection.remove_callback('fiducial') | |
689 | + if self.pedal_connection is not None: | |
690 | + self.pedal_connection.remove_callback('fiducial') | |
1078 | 691 | |
1079 | 692 | ctrl.SetValue(False) |
1080 | 693 | self.tracker_fiducial_being_set = None |
1081 | 694 | |
1082 | 695 | if ctrl.GetValue(): |
1083 | 696 | self.tracker_fiducial_being_set = n |
1084 | - self.pedal_connection.add_callback('fiducial', set_fiducial_callback) | |
697 | + if self.pedal_connection is not None: | |
698 | + self.pedal_connection.add_callback('fiducial', set_fiducial_callback) | |
1085 | 699 | else: |
1086 | 700 | set_fiducial_callback() |
1087 | 701 | |
... | ... | @@ -1195,7 +809,7 @@ class NeuronavigationPanel(wx.Panel): |
1195 | 809 | |
1196 | 810 | |
1197 | 811 | class ObjectRegistrationPanel(wx.Panel): |
1198 | - def __init__(self, parent, tracker): | |
812 | + def __init__(self, parent, tracker, pedal_connection): | |
1199 | 813 | wx.Panel.__init__(self, parent) |
1200 | 814 | try: |
1201 | 815 | default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) |
... | ... | @@ -1206,6 +820,7 @@ class ObjectRegistrationPanel(wx.Panel): |
1206 | 820 | self.coil_list = const.COIL |
1207 | 821 | |
1208 | 822 | self.tracker = tracker |
823 | + self.pedal_connection = pedal_connection | |
1209 | 824 | |
1210 | 825 | self.nav_prop = None |
1211 | 826 | self.obj_fiducials = None |
... | ... | @@ -1368,7 +983,7 @@ class ObjectRegistrationPanel(wx.Panel): |
1368 | 983 | def OnLinkCreate(self, event=None): |
1369 | 984 | |
1370 | 985 | if self.tracker.IsTrackerInitialized(): |
1371 | - dialog = dlg.ObjectCalibrationDialog(self.tracker) | |
986 | + dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection) | |
1372 | 987 | try: |
1373 | 988 | if dialog.ShowModal() == wx.ID_OK: |
1374 | 989 | self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue() |
... | ... | @@ -1479,7 +1094,6 @@ class MarkersPanel(wx.Panel): |
1479 | 1094 | self.current_angle = 0, 0, 0 |
1480 | 1095 | self.current_seed = 0, 0, 0 |
1481 | 1096 | self.list_coord = [] |
1482 | - self.marker_ind = 0 | |
1483 | 1097 | self.tgt_flag = self.tgt_index = None |
1484 | 1098 | self.nav_status = False |
1485 | 1099 | |
... | ... | @@ -1525,7 +1139,7 @@ class MarkersPanel(wx.Panel): |
1525 | 1139 | |
1526 | 1140 | # Buttons to delete or remove markers |
1527 | 1141 | btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23)) |
1528 | - btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteSingleMarker) | |
1142 | + btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteMultipleMarkers) | |
1529 | 1143 | |
1530 | 1144 | btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23)) |
1531 | 1145 | btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers) |
... | ... | @@ -1564,7 +1178,7 @@ class MarkersPanel(wx.Panel): |
1564 | 1178 | def __bind_events(self): |
1565 | 1179 | # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points') |
1566 | 1180 | Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point') |
1567 | - Publisher.subscribe(self.OnDeleteSingleMarker, 'Delete fiducial marker') | |
1181 | + Publisher.subscribe(self.OnDeleteMultipleMarkers, 'Delete fiducial marker') | |
1568 | 1182 | Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') |
1569 | 1183 | Publisher.subscribe(self.CreateMarker, 'Create marker') |
1570 | 1184 | Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') |
... | ... | @@ -1680,7 +1294,6 @@ class MarkersPanel(wx.Panel): |
1680 | 1294 | |
1681 | 1295 | if result == wx.ID_OK: |
1682 | 1296 | self.list_coord = [] |
1683 | - self.marker_ind = 0 | |
1684 | 1297 | Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount()) |
1685 | 1298 | self.lc.DeleteAllItems() |
1686 | 1299 | Publisher.sendMessage('Stop Blink Marker', index='DeleteAll') |
... | ... | @@ -1691,46 +1304,47 @@ class MarkersPanel(wx.Panel): |
1691 | 1304 | if not hasattr(evt, 'data'): |
1692 | 1305 | wx.MessageBox(_("Target deleted."), _("InVesalius 3")) |
1693 | 1306 | |
1694 | - def OnDeleteSingleMarker(self, evt=None, marker_id=None): | |
1695 | - # OnDeleteSingleMarker is used for both pubsub and button click events | |
1307 | + def OnDeleteMultipleMarkers(self, evt=None, label=None): | |
1308 | + # OnDeleteMultipleMarkers is used for both pubsub and button click events | |
1696 | 1309 | # Pubsub is used for fiducial handle and button click for all others |
1697 | 1310 | |
1698 | - if not evt: | |
1699 | - if self.lc.GetItemCount(): | |
1311 | + if not evt: # called through pubsub | |
1312 | + index = [] | |
1313 | + allowed_labels = itertools.chain(*(const.BTNS_IMG_MARKERS[i].values() for i in const.BTNS_IMG_MARKERS)) | |
1314 | + | |
1315 | + if label and (label in allowed_labels): | |
1700 | 1316 | for id_n in range(self.lc.GetItemCount()): |
1701 | 1317 | item = self.lc.GetItem(id_n, 4) |
1702 | - if item.GetText() == marker_id: | |
1703 | - for i in const.BTNS_IMG_MARKERS: | |
1704 | - if marker_id in list(const.BTNS_IMG_MARKERS[i].values())[0]: | |
1705 | - self.lc.Focus(item.GetId()) | |
1706 | - index = [self.lc.GetFocusedItem()] | |
1707 | - else: | |
1708 | - if self.lc.GetFirstSelected() != -1: | |
1709 | - index = self.GetSelectedItems() | |
1710 | - else: | |
1711 | - index = None | |
1318 | + if item.GetText() == label: | |
1319 | + self.lc.Focus(item.GetId()) | |
1320 | + index = [self.lc.GetFocusedItem()] | |
1321 | + | |
1322 | + else: # called from button click | |
1323 | + index = self.__getSelectedItems() | |
1712 | 1324 | |
1713 | - #TODO: There are bugs when no marker is selected, test and improve | |
1325 | + #TODO: Bug - when deleting multiple markers and target is not the first marker | |
1714 | 1326 | if index: |
1715 | 1327 | if self.tgt_flag and self.tgt_index == index[0]: |
1716 | 1328 | self.tgt_flag = self.tgt_index = None |
1717 | 1329 | Publisher.sendMessage('Disable or enable coil tracker', status=False) |
1718 | - wx.MessageBox(_("No data selected."), _("InVesalius 3")) | |
1330 | + wx.MessageBox(_("Target deleted."), _("InVesalius 3")) | |
1719 | 1331 | |
1720 | - self.DeleteMarker(index) | |
1332 | + self.__deleteMultipleMarkers(index) | |
1721 | 1333 | else: |
1722 | - wx.MessageBox(_("Target deleted."), _("InVesalius 3")) | |
1334 | + wx.MessageBox(_("No data selected."), _("InVesalius 3")) | |
1723 | 1335 | |
1724 | - def DeleteMarker(self, index): | |
1336 | + def __deleteMultipleMarkers(self, index): | |
1337 | + """ Delete multiple markers indexed by index. index must be sorted in | |
1338 | + the ascending order. | |
1339 | + """ | |
1725 | 1340 | for i in reversed(index): |
1726 | 1341 | del self.list_coord[i] |
1727 | 1342 | self.lc.DeleteItem(i) |
1728 | 1343 | for n in range(0, self.lc.GetItemCount()): |
1729 | 1344 | self.lc.SetItem(n, 0, str(n+1)) |
1730 | - self.marker_ind -= 1 | |
1731 | 1345 | Publisher.sendMessage('Remove marker', index=index) |
1732 | 1346 | |
1733 | - def OnCreateMarker(self, evt=None, coord=None, marker_id=None, colour=None): | |
1347 | + def OnCreateMarker(self, evt): | |
1734 | 1348 | self.CreateMarker() |
1735 | 1349 | |
1736 | 1350 | def OnLoadMarkers(self, evt): |
... | ... | @@ -1792,7 +1406,7 @@ class MarkersPanel(wx.Panel): |
1792 | 1406 | marker_id = '*' |
1793 | 1407 | |
1794 | 1408 | self.CreateMarker(coord=coord, colour=colour, size=size, |
1795 | - marker_id=marker_id, target_id=target_id, seed=seed) | |
1409 | + label=marker_id, target_id=target_id, seed=seed) | |
1796 | 1410 | |
1797 | 1411 | # if there are multiple TARGETS will set the last one |
1798 | 1412 | if target: |
... | ... | @@ -1847,7 +1461,7 @@ class MarkersPanel(wx.Panel): |
1847 | 1461 | target_id = line[20] |
1848 | 1462 | |
1849 | 1463 | self.CreateMarker(coord=coord, colour=colour, size=size, |
1850 | - marker_id=marker_id, target_id=target_id, seed=seed) | |
1464 | + label=marker_id, target_id=target_id, seed=seed) | |
1851 | 1465 | |
1852 | 1466 | # if there are multiple TARGETS will set the last one |
1853 | 1467 | if target: |
... | ... | @@ -1903,7 +1517,7 @@ class MarkersPanel(wx.Panel): |
1903 | 1517 | def OnSelectSize(self, evt, ctrl): |
1904 | 1518 | self.marker_size = ctrl.GetValue() |
1905 | 1519 | |
1906 | - def CreateMarker(self, coord=None, colour=None, size=None, marker_id='*', target_id='*', seed=None): | |
1520 | + def CreateMarker(self, coord=None, colour=None, size=None, label='*', target_id='*', seed=None): | |
1907 | 1521 | coord = coord or self.current_coord |
1908 | 1522 | colour = colour or self.marker_colour |
1909 | 1523 | size = size or self.marker_size |
... | ... | @@ -1917,9 +1531,7 @@ class MarkersPanel(wx.Panel): |
1917 | 1531 | # TODO: Use matrix coordinates and not world coordinates as current method. |
1918 | 1532 | # This makes easier for inter-software comprehension. |
1919 | 1533 | |
1920 | - Publisher.sendMessage('Add marker', ball_id=self.marker_ind, size=size, colour=colour, coord=coord[0:3]) | |
1921 | - | |
1922 | - self.marker_ind += 1 | |
1534 | + Publisher.sendMessage('Add marker', ball_id=len(self.list_coord), size=size, colour=colour, coord=coord[0:3]) | |
1923 | 1535 | |
1924 | 1536 | # List of lists with coordinates and properties of a marker |
1925 | 1537 | line = [] |
... | ... | @@ -1928,7 +1540,7 @@ class MarkersPanel(wx.Panel): |
1928 | 1540 | line.extend(orientation_world) |
1929 | 1541 | line.extend(colour) |
1930 | 1542 | line.append(size) |
1931 | - line.append(marker_id) | |
1543 | + line.append(label) | |
1932 | 1544 | line.extend(seed) |
1933 | 1545 | line.append(target_id) |
1934 | 1546 | |
... | ... | @@ -1944,21 +1556,24 @@ class MarkersPanel(wx.Panel): |
1944 | 1556 | self.lc.SetItem(num_items, 1, str(round(coord[0], 2))) |
1945 | 1557 | self.lc.SetItem(num_items, 2, str(round(coord[1], 2))) |
1946 | 1558 | self.lc.SetItem(num_items, 3, str(round(coord[2], 2))) |
1947 | - self.lc.SetItem(num_items, 4, str(marker_id)) | |
1559 | + self.lc.SetItem(num_items, 4, str(label)) | |
1948 | 1560 | self.lc.EnsureVisible(num_items) |
1949 | 1561 | |
1950 | - def GetSelectedItems(self): | |
1562 | + def __getSelectedItems(self): | |
1951 | 1563 | """ |
1952 | - Returns a list of the selected items in the list control. | |
1564 | + Returns a (possibly empty) list of the selected items in the list control. | |
1953 | 1565 | """ |
1954 | 1566 | selection = [] |
1955 | - index = self.lc.GetFirstSelected() | |
1956 | - selection.append(index) | |
1957 | - while len(selection) != self.lc.GetSelectedItemCount(): | |
1958 | - index = self.lc.GetNextSelected(index) | |
1959 | - selection.append(index) | |
1567 | + | |
1568 | + next = self.lc.GetFirstSelected() | |
1569 | + | |
1570 | + while next != -1: | |
1571 | + selection.append(next) | |
1572 | + next = self.lc.GetNextSelected(next) | |
1573 | + | |
1960 | 1574 | return selection |
1961 | 1575 | |
1576 | + | |
1962 | 1577 | class DbsPanel(wx.Panel): |
1963 | 1578 | def __init__(self, parent): |
1964 | 1579 | wx.Panel.__init__(self, parent) |
... | ... | @@ -2410,100 +2025,6 @@ class TractographyPanel(wx.Panel): |
2410 | 2025 | Publisher.sendMessage('Remove tracts') |
2411 | 2026 | |
2412 | 2027 | |
2413 | -class QueueCustom(queue.Queue): | |
2414 | - """ | |
2415 | - A custom queue subclass that provides a :meth:`clear` method. | |
2416 | - https://stackoverflow.com/questions/6517953/clear-all-items-from-the-queue | |
2417 | - Modified to a LIFO Queue type (Last-in-first-out). Seems to make sense for the navigation | |
2418 | - threads, as the last added coordinate should be the first to be processed. | |
2419 | - In the first tests in a short run, seems to increase the coord queue size considerably, | |
2420 | - possibly limiting the queue size is good. | |
2421 | - """ | |
2422 | - | |
2423 | - def clear(self): | |
2424 | - """ | |
2425 | - Clears all items from the queue. | |
2426 | - """ | |
2427 | - | |
2428 | - with self.mutex: | |
2429 | - unfinished = self.unfinished_tasks - len(self.queue) | |
2430 | - if unfinished <= 0: | |
2431 | - if unfinished < 0: | |
2432 | - raise ValueError('task_done() called too many times') | |
2433 | - self.all_tasks_done.notify_all() | |
2434 | - self.unfinished_tasks = unfinished | |
2435 | - self.queue.clear() | |
2436 | - self.not_full.notify_all() | |
2437 | - | |
2438 | - | |
2439 | -class UpdateNavigationScene(threading.Thread): | |
2440 | - | |
2441 | - def __init__(self, vis_queues, vis_components, event, sle): | |
2442 | - """Class (threading) to update the navigation scene with all graphical elements. | |
2443 | - | |
2444 | - Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation | |
2445 | - | |
2446 | - :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene | |
2447 | - :type affine_vtk: vtkMatrix4x4 | |
2448 | - :param visualization_queue: Queue instance that manage coordinates to be visualized | |
2449 | - :type visualization_queue: queue.Queue | |
2450 | - :param event: Threading event to coordinate when tasks as done and allow UI release | |
2451 | - :type event: threading.Event | |
2452 | - :param sle: Sleep pause in seconds | |
2453 | - :type sle: float | |
2454 | - """ | |
2455 | - | |
2456 | - threading.Thread.__init__(self, name='UpdateScene') | |
2457 | - self.serial_port_enabled, self.view_tracts, self.peel_loaded = vis_components | |
2458 | - self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue = vis_queues | |
2459 | - self.sle = sle | |
2460 | - self.event = event | |
2461 | - | |
2462 | - def run(self): | |
2463 | - # count = 0 | |
2464 | - while not self.event.is_set(): | |
2465 | - got_coords = False | |
2466 | - try: | |
2467 | - coord, m_img, view_obj = self.coord_queue.get_nowait() | |
2468 | - got_coords = True | |
2469 | - | |
2470 | - # print('UpdateScene: get {}'.format(count)) | |
2471 | - | |
2472 | - # use of CallAfter is mandatory otherwise crashes the wx interface | |
2473 | - if self.view_tracts: | |
2474 | - bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait() | |
2475 | - #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command | |
2476 | - wx.CallAfter(Publisher.sendMessage, 'Remove tracts') | |
2477 | - wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle, | |
2478 | - affine_vtk=affine_vtk, coord_offset=coord_offset) | |
2479 | - # wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset) | |
2480 | - self.tracts_queue.task_done() | |
2481 | - | |
2482 | - if self.serial_port_enabled: | |
2483 | - trigger_on = self.serial_port_queue.get_nowait() | |
2484 | - if trigger_on: | |
2485 | - wx.CallAfter(Publisher.sendMessage, 'Create marker') | |
2486 | - self.serial_port_queue.task_done() | |
2487 | - | |
2488 | - #TODO: If using the view_tracts substitute the raw coord from the offset coordinate, so the user | |
2489 | - # see the red cross in the position of the offset marker | |
2490 | - wx.CallAfter(Publisher.sendMessage, 'Update slices position', position=coord[:3]) | |
2491 | - wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord) | |
2492 | - wx.CallAfter(Publisher.sendMessage, 'Update slice viewer') | |
2493 | - | |
2494 | - if view_obj: | |
2495 | - wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord) | |
2496 | - wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix',m_img=m_img, coord=coord, flag= self.peel_loaded) | |
2497 | - self.coord_queue.task_done() | |
2498 | - # print('UpdateScene: done {}'.format(count)) | |
2499 | - # count += 1 | |
2500 | - | |
2501 | - sleep(self.sle) | |
2502 | - except queue.Empty: | |
2503 | - if got_coords: | |
2504 | - self.coord_queue.task_done() | |
2505 | - | |
2506 | - | |
2507 | 2028 | class InputAttributes(object): |
2508 | 2029 | # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python |
2509 | 2030 | def __init__(self, *initial_data, **kwargs): | ... | ... |
... | ... | @@ -0,0 +1,80 @@ |
1 | +#-------------------------------------------------------------------------- | |
2 | +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas | |
3 | +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer | |
4 | +# Homepage: http://www.softwarepublico.gov.br | |
5 | +# Contact: invesalius@cti.gov.br | |
6 | +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) | |
7 | +#-------------------------------------------------------------------------- | |
8 | +# Este programa e software livre; voce pode redistribui-lo e/ou | |
9 | +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme | |
10 | +# publicada pela Free Software Foundation; de acordo com a versao 2 | |
11 | +# da Licenca. | |
12 | +# | |
13 | +# Este programa eh distribuido na expectativa de ser util, mas SEM | |
14 | +# QUALQUER GARANTIA; sem mesmo a garantia implicita de | |
15 | +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM | |
16 | +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais | |
17 | +# detalhes. | |
18 | +#-------------------------------------------------------------------------- | |
19 | + | |
20 | +import wx | |
21 | + | |
22 | +import invesalius.data.bases as db | |
23 | +import invesalius.gui.dialogs as dlg | |
24 | +from invesalius.pubsub import pub as Publisher | |
25 | + | |
26 | + | |
27 | +class ICP(): | |
28 | + def __init__(self): | |
29 | + self.use_icp = False | |
30 | + self.m_icp = None | |
31 | + self.icp_fre = None | |
32 | + | |
33 | + def StartICP(self, navigation, tracker): | |
34 | + ref_mode_id = navigation.GetReferenceMode() | |
35 | + | |
36 | + if not self.use_icp: | |
37 | + if dlg.ICPcorregistration(navigation.fre): | |
38 | + Publisher.sendMessage('Stop navigation') | |
39 | + use_icp, self.m_icp = self.OnICP(navigation, tracker, navigation.m_change) | |
40 | + if use_icp: | |
41 | + self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials, | |
42 | + ref_mode_id, navigation.m_change, self.m_icp) | |
43 | + self.SetICP(navigation, use_icp) | |
44 | + else: | |
45 | + print("ICP canceled") | |
46 | + Publisher.sendMessage('Start navigation') | |
47 | + | |
48 | + def OnICP(self, navigation, tracker, m_change): | |
49 | + ref_mode_id = navigation.GetReferenceMode() | |
50 | + | |
51 | + dialog = dlg.ICPCorregistrationDialog(nav_prop=(m_change, tracker.tracker_id, tracker.trk_init, ref_mode_id)) | |
52 | + | |
53 | + if dialog.ShowModal() == wx.ID_OK: | |
54 | + m_icp, point_coord, transformed_points, prev_error, final_error = dialog.GetValue() | |
55 | + # TODO: checkbox in the dialog to transfer the icp points to 3D viewer | |
56 | + #create markers | |
57 | + # for i in range(len(point_coord)): | |
58 | + # img_coord = point_coord[i][0],-point_coord[i][1],point_coord[i][2], 0, 0, 0 | |
59 | + # transf_coord = transformed_points[i][0],-transformed_points[i][1],transformed_points[i][2], 0, 0, 0 | |
60 | + # Publisher.sendMessage('Create marker', coord=img_coord, marker_id=None, colour=(1,0,0)) | |
61 | + # Publisher.sendMessage('Create marker', coord=transf_coord, marker_id=None, colour=(0,0,1)) | |
62 | + if m_icp is not None: | |
63 | + dlg.ReportICPerror(prev_error, final_error) | |
64 | + use_icp = True | |
65 | + else: | |
66 | + use_icp = False | |
67 | + | |
68 | + return use_icp, m_icp | |
69 | + | |
70 | + else: | |
71 | + return self.use_icp, self.m_icp | |
72 | + | |
73 | + def SetICP(self, navigation, use_icp): | |
74 | + self.use_icp = use_icp | |
75 | + navigation.icp_queue.put_nowait([self.use_icp, self.m_icp]) | |
76 | + | |
77 | + def ResetICP(self): | |
78 | + self.use_icp = False | |
79 | + self.m_icp = None | |
80 | + self.icp_fre = None | ... | ... |
... | ... | @@ -0,0 +1,341 @@ |
1 | +#-------------------------------------------------------------------------- | |
2 | +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas | |
3 | +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer | |
4 | +# Homepage: http://www.softwarepublico.gov.br | |
5 | +# Contact: invesalius@cti.gov.br | |
6 | +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) | |
7 | +#-------------------------------------------------------------------------- | |
8 | +# Este programa e software livre; voce pode redistribui-lo e/ou | |
9 | +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme | |
10 | +# publicada pela Free Software Foundation; de acordo com a versao 2 | |
11 | +# da Licenca. | |
12 | +# | |
13 | +# Este programa eh distribuido na expectativa de ser util, mas SEM | |
14 | +# QUALQUER GARANTIA; sem mesmo a garantia implicita de | |
15 | +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM | |
16 | +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais | |
17 | +# detalhes. | |
18 | +#-------------------------------------------------------------------------- | |
19 | + | |
20 | +import threading | |
21 | +import queue | |
22 | +from time import sleep | |
23 | + | |
24 | +import wx | |
25 | +import numpy as np | |
26 | + | |
27 | +import invesalius.constants as const | |
28 | +import invesalius.project as prj | |
29 | +import invesalius.data.bases as db | |
30 | +import invesalius.data.coordinates as dco | |
31 | +import invesalius.data.coregistration as dcr | |
32 | +import invesalius.data.serial_port_connection as spc | |
33 | +import invesalius.data.slice_ as sl | |
34 | +import invesalius.data.tractography as dti | |
35 | +import invesalius.data.transformations as tr | |
36 | +import invesalius.data.vtk_utils as vtk_utils | |
37 | +from invesalius.pubsub import pub as Publisher | |
38 | + | |
39 | + | |
40 | +class QueueCustom(queue.Queue): | |
41 | + """ | |
42 | + A custom queue subclass that provides a :meth:`clear` method. | |
43 | + https://stackoverflow.com/questions/6517953/clear-all-items-from-the-queue | |
44 | + Modified to a LIFO Queue type (Last-in-first-out). Seems to make sense for the navigation | |
45 | + threads, as the last added coordinate should be the first to be processed. | |
46 | + In the first tests in a short run, seems to increase the coord queue size considerably, | |
47 | + possibly limiting the queue size is good. | |
48 | + """ | |
49 | + | |
50 | + def clear(self): | |
51 | + """ | |
52 | + Clears all items from the queue. | |
53 | + """ | |
54 | + | |
55 | + with self.mutex: | |
56 | + unfinished = self.unfinished_tasks - len(self.queue) | |
57 | + if unfinished <= 0: | |
58 | + if unfinished < 0: | |
59 | + raise ValueError('task_done() called too many times') | |
60 | + self.all_tasks_done.notify_all() | |
61 | + self.unfinished_tasks = unfinished | |
62 | + self.queue.clear() | |
63 | + self.not_full.notify_all() | |
64 | + | |
65 | + | |
66 | +class UpdateNavigationScene(threading.Thread): | |
67 | + | |
68 | + def __init__(self, vis_queues, vis_components, event, sle): | |
69 | + """Class (threading) to update the navigation scene with all graphical elements. | |
70 | + | |
71 | + Sleep function in run method is used to avoid blocking GUI and more fluent, real-time navigation | |
72 | + | |
73 | + :param affine_vtk: Affine matrix in vtkMatrix4x4 instance to update objects position in 3D scene | |
74 | + :type affine_vtk: vtkMatrix4x4 | |
75 | + :param visualization_queue: Queue instance that manage coordinates to be visualized | |
76 | + :type visualization_queue: queue.Queue | |
77 | + :param event: Threading event to coordinate when tasks as done and allow UI release | |
78 | + :type event: threading.Event | |
79 | + :param sle: Sleep pause in seconds | |
80 | + :type sle: float | |
81 | + """ | |
82 | + | |
83 | + threading.Thread.__init__(self, name='UpdateScene') | |
84 | + self.serial_port_enabled, self.view_tracts, self.peel_loaded = vis_components | |
85 | + self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue = vis_queues | |
86 | + self.sle = sle | |
87 | + self.event = event | |
88 | + | |
89 | + def run(self): | |
90 | + # count = 0 | |
91 | + while not self.event.is_set(): | |
92 | + got_coords = False | |
93 | + try: | |
94 | + coord, m_img, view_obj = self.coord_queue.get_nowait() | |
95 | + got_coords = True | |
96 | + | |
97 | + # print('UpdateScene: get {}'.format(count)) | |
98 | + | |
99 | + # use of CallAfter is mandatory otherwise crashes the wx interface | |
100 | + if self.view_tracts: | |
101 | + bundle, affine_vtk, coord_offset = self.tracts_queue.get_nowait() | |
102 | + #TODO: Check if possible to combine the Remove tracts with Update tracts in a single command | |
103 | + wx.CallAfter(Publisher.sendMessage, 'Remove tracts') | |
104 | + wx.CallAfter(Publisher.sendMessage, 'Update tracts', root=bundle, | |
105 | + affine_vtk=affine_vtk, coord_offset=coord_offset) | |
106 | + # wx.CallAfter(Publisher.sendMessage, 'Update marker offset', coord_offset=coord_offset) | |
107 | + self.tracts_queue.task_done() | |
108 | + | |
109 | + if self.serial_port_enabled: | |
110 | + trigger_on = self.serial_port_queue.get_nowait() | |
111 | + if trigger_on: | |
112 | + wx.CallAfter(Publisher.sendMessage, 'Create marker') | |
113 | + self.serial_port_queue.task_done() | |
114 | + | |
115 | + #TODO: If using the view_tracts substitute the raw coord from the offset coordinate, so the user | |
116 | + # see the red cross in the position of the offset marker | |
117 | + wx.CallAfter(Publisher.sendMessage, 'Update slices position', position=coord[:3]) | |
118 | + wx.CallAfter(Publisher.sendMessage, 'Set cross focal point', position=coord) | |
119 | + wx.CallAfter(Publisher.sendMessage, 'Update slice viewer') | |
120 | + | |
121 | + if view_obj: | |
122 | + wx.CallAfter(Publisher.sendMessage, 'Update object matrix', m_img=m_img, coord=coord) | |
123 | + wx.CallAfter(Publisher.sendMessage, 'Update object arrow matrix',m_img=m_img, coord=coord, flag= self.peel_loaded) | |
124 | + self.coord_queue.task_done() | |
125 | + # print('UpdateScene: done {}'.format(count)) | |
126 | + # count += 1 | |
127 | + | |
128 | + sleep(self.sle) | |
129 | + except queue.Empty: | |
130 | + if got_coords: | |
131 | + self.coord_queue.task_done() | |
132 | + | |
133 | + | |
134 | +class Navigation(): | |
135 | + def __init__(self, pedal_connection): | |
136 | + self.pedal_connection = pedal_connection | |
137 | + | |
138 | + self.image_fiducials = np.full([3, 3], np.nan) | |
139 | + self.correg = None | |
140 | + self.target = None | |
141 | + self.obj_reg = None | |
142 | + self.track_obj = False | |
143 | + self.m_change = None | |
144 | + self.all_fiducials = np.zeros((6, 6)) | |
145 | + | |
146 | + self.event = threading.Event() | |
147 | + self.coord_queue = QueueCustom(maxsize=1) | |
148 | + self.icp_queue = QueueCustom(maxsize=1) | |
149 | + # self.visualization_queue = QueueCustom(maxsize=1) | |
150 | + self.serial_port_queue = QueueCustom(maxsize=1) | |
151 | + self.coord_tracts_queue = QueueCustom(maxsize=1) | |
152 | + self.tracts_queue = QueueCustom(maxsize=1) | |
153 | + | |
154 | + # Tracker parameters | |
155 | + self.ref_mode_id = const.DEFAULT_REF_MODE | |
156 | + | |
157 | + # Tractography parameters | |
158 | + self.trk_inp = None | |
159 | + self.trekker = None | |
160 | + self.n_threads = None | |
161 | + self.view_tracts = False | |
162 | + self.peel_loaded = False | |
163 | + self.enable_act = False | |
164 | + self.act_data = None | |
165 | + self.n_tracts = const.N_TRACTS | |
166 | + self.seed_offset = const.SEED_OFFSET | |
167 | + self.seed_radius = const.SEED_RADIUS | |
168 | + self.sleep_nav = const.SLEEP_NAVIGATION | |
169 | + | |
170 | + # Serial port | |
171 | + self.serial_port = None | |
172 | + self.serial_port_connection = None | |
173 | + | |
174 | + # During navigation | |
175 | + self.coil_at_target = False | |
176 | + | |
177 | + self.__bind_events() | |
178 | + | |
179 | + def __bind_events(self): | |
180 | + Publisher.subscribe(self.CoilAtTarget, 'Coil at target') | |
181 | + | |
182 | + def CoilAtTarget(self, state): | |
183 | + self.coil_at_target = state | |
184 | + | |
185 | + def UpdateSleep(self, sleep): | |
186 | + self.sleep_nav = sleep | |
187 | + self.serial_port_connection.sleep_nav = sleep | |
188 | + | |
189 | + def SerialPortEnabled(self): | |
190 | + return self.serial_port is not None | |
191 | + | |
192 | + def SetReferenceMode(self, value): | |
193 | + self.ref_mode_id = value | |
194 | + | |
195 | + def GetReferenceMode(self): | |
196 | + return self.ref_mode_id | |
197 | + | |
198 | + def SetImageFiducial(self, fiducial_index, coord): | |
199 | + self.image_fiducials[fiducial_index, :] = coord | |
200 | + | |
201 | + print("Set image fiducial {} to coordinates {}".format(fiducial_index, coord)) | |
202 | + | |
203 | + def AreImageFiducialsSet(self): | |
204 | + return not np.isnan(self.image_fiducials).any() | |
205 | + | |
206 | + def UpdateFiducialRegistrationError(self, tracker): | |
207 | + tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials() | |
208 | + | |
209 | + self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials]) | |
210 | + | |
211 | + self.fre = db.calculate_fre(tracker_fiducials_raw, self.all_fiducials, self.ref_mode_id, self.m_change) | |
212 | + | |
213 | + def GetFiducialRegistrationError(self, icp): | |
214 | + fre = icp.icp_fre if icp.use_icp else self.fre | |
215 | + return fre, fre <= const.FIDUCIAL_REGISTRATION_ERROR_THRESHOLD | |
216 | + | |
217 | + def PedalStateChanged(self, state): | |
218 | + if state is True and self.coil_at_target and self.SerialPortEnabled(): | |
219 | + self.serial_port_connection.SendPulse() | |
220 | + | |
221 | + def StartNavigation(self, tracker): | |
222 | + tracker_fiducials, tracker_fiducials_raw = tracker.GetTrackerFiducials() | |
223 | + | |
224 | + # initialize jobs list | |
225 | + jobs_list = [] | |
226 | + | |
227 | + if self.event.is_set(): | |
228 | + self.event.clear() | |
229 | + | |
230 | + vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded] | |
231 | + vis_queues = [self.coord_queue, self.serial_port_queue, self.tracts_queue, self.icp_queue] | |
232 | + | |
233 | + Publisher.sendMessage("Navigation status", nav_status=True, vis_status=vis_components) | |
234 | + | |
235 | + self.all_fiducials = np.vstack([self.image_fiducials, tracker_fiducials]) | |
236 | + | |
237 | + # fiducials matrix | |
238 | + m_change = tr.affine_matrix_from_points(self.all_fiducials[3:, :].T, self.all_fiducials[:3, :].T, | |
239 | + shear=False, scale=False) | |
240 | + self.m_change = m_change | |
241 | + | |
242 | + errors = False | |
243 | + | |
244 | + if self.track_obj: | |
245 | + # if object tracking is selected | |
246 | + if self.obj_reg is None: | |
247 | + # check if object registration was performed | |
248 | + wx.MessageBox(_("Perform coil registration before navigation."), _("InVesalius 3")) | |
249 | + errors = True | |
250 | + else: | |
251 | + # if object registration was correctly performed continue with navigation | |
252 | + # obj_reg[0] is object 3x3 fiducial matrix and obj_reg[1] is 3x3 orientation matrix | |
253 | + obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.obj_reg | |
254 | + | |
255 | + coreg_data = [m_change, obj_ref_mode] | |
256 | + | |
257 | + if self.ref_mode_id: | |
258 | + coord_raw = dco.GetCoordinates(tracker.trk_init, tracker.tracker_id, self.ref_mode_id) | |
259 | + else: | |
260 | + coord_raw = np.array([None]) | |
261 | + | |
262 | + obj_data = db.object_registration(obj_fiducials, obj_orients, coord_raw, m_change) | |
263 | + coreg_data.extend(obj_data) | |
264 | + | |
265 | + queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue] | |
266 | + jobs_list.append(dcr.CoordinateCorregistrate(self.ref_mode_id, tracker, coreg_data, | |
267 | + self.view_tracts, queues, | |
268 | + self.event, self.sleep_nav, tracker.tracker_id, | |
269 | + self.target)) | |
270 | + else: | |
271 | + coreg_data = (m_change, 0) | |
272 | + queues = [self.coord_queue, self.coord_tracts_queue, self.icp_queue] | |
273 | + jobs_list.append(dcr.CoordinateCorregistrateNoObject(self.ref_mode_id, tracker, coreg_data, | |
274 | + self.view_tracts, queues, | |
275 | + self.event, self.sleep_nav)) | |
276 | + | |
277 | + if not errors: | |
278 | + #TODO: Test the serial port thread | |
279 | + if self.SerialPortEnabled(): | |
280 | + self.serial_port_connection = spc.SerialPortConnection( | |
281 | + self.serial_port, | |
282 | + self.serial_port_queue, | |
283 | + self.event, | |
284 | + self.sleep_nav, | |
285 | + ) | |
286 | + self.serial_port_connection.Connect() | |
287 | + jobs_list.append(self.serial_port_connection) | |
288 | + | |
289 | + if self.view_tracts: | |
290 | + # initialize Trekker parameters | |
291 | + slic = sl.Slice() | |
292 | + prj_data = prj.Project() | |
293 | + matrix_shape = tuple(prj_data.matrix_shape) | |
294 | + affine = slic.affine.copy() | |
295 | + affine[1, -1] -= matrix_shape[1] | |
296 | + affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) | |
297 | + Publisher.sendMessage("Update marker offset state", create=True) | |
298 | + self.trk_inp = self.trekker, affine, self.seed_offset, self.n_tracts, self.seed_radius,\ | |
299 | + self.n_threads, self.act_data, affine_vtk, matrix_shape[1] | |
300 | + # print("Appending the tract computation thread!") | |
301 | + queues = [self.coord_tracts_queue, self.tracts_queue] | |
302 | + if self.enable_act: | |
303 | + jobs_list.append(dti.ComputeTractsACTThread(self.trk_inp, queues, self.event, self.sleep_nav)) | |
304 | + else: | |
305 | + jobs_list.append(dti.ComputeTractsThread(self.trk_inp, queues, self.event, self.sleep_nav)) | |
306 | + | |
307 | + jobs_list.append(UpdateNavigationScene(vis_queues, vis_components, | |
308 | + self.event, self.sleep_nav)) | |
309 | + | |
310 | + for jobs in jobs_list: | |
311 | + # jobs.daemon = True | |
312 | + jobs.start() | |
313 | + # del jobs | |
314 | + | |
315 | + if self.pedal_connection is not None: | |
316 | + self.pedal_connection.add_callback('navigation', self.PedalStateChanged) | |
317 | + | |
318 | + def StopNavigation(self): | |
319 | + self.event.set() | |
320 | + | |
321 | + if self.pedal_connection is not None: | |
322 | + self.pedal_connection.remove_callback('navigation') | |
323 | + | |
324 | + self.coord_queue.clear() | |
325 | + self.coord_queue.join() | |
326 | + | |
327 | + if self.SerialPortEnabled(): | |
328 | + self.serial_port_connection.join() | |
329 | + | |
330 | + self.serial_port_queue.clear() | |
331 | + self.serial_port_queue.join() | |
332 | + | |
333 | + if self.view_tracts: | |
334 | + self.coord_tracts_queue.clear() | |
335 | + self.coord_tracts_queue.join() | |
336 | + | |
337 | + self.tracts_queue.clear() | |
338 | + self.tracts_queue.join() | |
339 | + | |
340 | + vis_components = [self.SerialPortEnabled(), self.view_tracts, self.peel_loaded] | |
341 | + Publisher.sendMessage("Navigation status", nav_status=False, vis_status=vis_components) | ... | ... |
... | ... | @@ -0,0 +1,143 @@ |
1 | +#-------------------------------------------------------------------------- | |
2 | +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas | |
3 | +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer | |
4 | +# Homepage: http://www.softwarepublico.gov.br | |
5 | +# Contact: invesalius@cti.gov.br | |
6 | +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) | |
7 | +#-------------------------------------------------------------------------- | |
8 | +# Este programa e software livre; voce pode redistribui-lo e/ou | |
9 | +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme | |
10 | +# publicada pela Free Software Foundation; de acordo com a versao 2 | |
11 | +# da Licenca. | |
12 | +# | |
13 | +# Este programa eh distribuido na expectativa de ser util, mas SEM | |
14 | +# QUALQUER GARANTIA; sem mesmo a garantia implicita de | |
15 | +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM | |
16 | +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais | |
17 | +# detalhes. | |
18 | +#-------------------------------------------------------------------------- | |
19 | + | |
20 | +import numpy as np | |
21 | + | |
22 | +import invesalius.constants as const | |
23 | +import invesalius.data.coordinates as dco | |
24 | +import invesalius.data.trackers as dt | |
25 | +import invesalius.gui.dialogs as dlg | |
26 | +from invesalius.pubsub import pub as Publisher | |
27 | + | |
28 | + | |
29 | +class Tracker(): | |
30 | + def __init__(self): | |
31 | + self.trk_init = None | |
32 | + self.tracker_id = const.DEFAULT_TRACKER | |
33 | + | |
34 | + self.tracker_fiducials = np.full([3, 3], np.nan) | |
35 | + self.tracker_fiducials_raw = np.zeros((6, 6)) | |
36 | + | |
37 | + self.tracker_connected = False | |
38 | + | |
39 | + def SetTracker(self, new_tracker): | |
40 | + if new_tracker: | |
41 | + self.DisconnectTracker() | |
42 | + | |
43 | + self.trk_init = dt.TrackerConnection(new_tracker, None, 'connect') | |
44 | + if not self.trk_init[0]: | |
45 | + dlg.ShowNavigationTrackerWarning(self.tracker_id, self.trk_init[1]) | |
46 | + | |
47 | + self.tracker_id = 0 | |
48 | + self.tracker_connected = False | |
49 | + else: | |
50 | + self.tracker_id = new_tracker | |
51 | + self.tracker_connected = True | |
52 | + | |
53 | + def DisconnectTracker(self): | |
54 | + if self.tracker_connected: | |
55 | + self.ResetTrackerFiducials() | |
56 | + Publisher.sendMessage('Update status text in GUI', | |
57 | + label=_("Disconnecting tracker ...")) | |
58 | + Publisher.sendMessage('Remove sensors ID') | |
59 | + Publisher.sendMessage('Remove object data') | |
60 | + self.trk_init = dt.TrackerConnection(self.tracker_id, self.trk_init[0], 'disconnect') | |
61 | + if not self.trk_init[0]: | |
62 | + self.tracker_connected = False | |
63 | + self.tracker_id = 0 | |
64 | + | |
65 | + Publisher.sendMessage('Update status text in GUI', | |
66 | + label=_("Tracker disconnected")) | |
67 | + print("Tracker disconnected!") | |
68 | + else: | |
69 | + Publisher.sendMessage('Update status text in GUI', | |
70 | + label=_("Tracker still connected")) | |
71 | + print("Tracker still connected!") | |
72 | + | |
73 | + def IsTrackerInitialized(self): | |
74 | + return self.trk_init and self.tracker_id and self.tracker_connected | |
75 | + | |
76 | + def AreTrackerFiducialsSet(self): | |
77 | + return not np.isnan(self.tracker_fiducials).any() | |
78 | + | |
79 | + def GetTrackerCoordinates(self, ref_mode_id, n_samples=1): | |
80 | + coord_raw_samples = {} | |
81 | + coord_samples = {} | |
82 | + | |
83 | + for i in range(n_samples): | |
84 | + coord_raw = dco.GetCoordinates(self.trk_init, self.tracker_id, ref_mode_id) | |
85 | + | |
86 | + if ref_mode_id == const.DYNAMIC_REF: | |
87 | + coord = dco.dynamic_reference_m(coord_raw[0, :], coord_raw[1, :]) | |
88 | + else: | |
89 | + coord = coord_raw[0, :] | |
90 | + coord[2] = -coord[2] | |
91 | + | |
92 | + coord_raw_samples[i] = coord_raw | |
93 | + coord_samples[i] = coord | |
94 | + | |
95 | + coord_raw_avg = np.median(list(coord_raw_samples.values()), axis=0) | |
96 | + coord_avg = np.median(list(coord_samples.values()), axis=0) | |
97 | + | |
98 | + return coord_avg, coord_raw_avg | |
99 | + | |
100 | + def SetTrackerFiducial(self, ref_mode_id, fiducial_index): | |
101 | + coord, coord_raw = self.GetTrackerCoordinates( | |
102 | + ref_mode_id=ref_mode_id, | |
103 | + n_samples=const.CALIBRATION_TRACKER_SAMPLES, | |
104 | + ) | |
105 | + | |
106 | + # Update tracker fiducial with tracker coordinates | |
107 | + self.tracker_fiducials[fiducial_index, :] = coord[0:3] | |
108 | + | |
109 | + assert 0 <= fiducial_index <= 2, "Fiducial index out of range (0-2): {}".format(fiducial_index) | |
110 | + | |
111 | + self.tracker_fiducials_raw[2 * fiducial_index, :] = coord_raw[0, :] | |
112 | + self.tracker_fiducials_raw[2 * fiducial_index + 1, :] = coord_raw[1, :] | |
113 | + | |
114 | + print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3])) | |
115 | + | |
116 | + def ResetTrackerFiducials(self): | |
117 | + for m in range(3): | |
118 | + self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan] | |
119 | + | |
120 | + def GetTrackerFiducials(self): | |
121 | + return self.tracker_fiducials, self.tracker_fiducials_raw | |
122 | + | |
123 | + def GetTrackerInfo(self): | |
124 | + return self.trk_init, self.tracker_id | |
125 | + | |
126 | + def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre): | |
127 | + if self.tracker_connected: | |
128 | + selection_ctrl.SetSelection(self.tracker_id) | |
129 | + else: | |
130 | + selection_ctrl.SetSelection(0) | |
131 | + | |
132 | + # Update tracker location in the UI. | |
133 | + for m in range(3): | |
134 | + coord = self.tracker_fiducials[m, :] | |
135 | + for n in range(0, 3): | |
136 | + value = 0.0 if np.isnan(coord[n]) else float(coord[n]) | |
137 | + numctrls_fiducial[m][n].SetValue(value) | |
138 | + | |
139 | + txtctrl_fre.SetValue('') | |
140 | + txtctrl_fre.SetBackgroundColour('WHITE') | |
141 | + | |
142 | + def get_trackers(self): | |
143 | + return const.TRACKERS | ... | ... |