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 | *.so | 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 | *.aux | 290 | *.aux |
291 | +*.lof | ||
292 | +*.log | ||
293 | +*.lot | ||
294 | +*.fls | ||
295 | +*.out | ||
41 | *.toc | 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 | *.bbl | 316 | *.bbl |
317 | +*.bcf | ||
43 | *.blg | 318 | *.blg |
44 | -*.fls | 319 | +*-blx.aux |
320 | +*-blx.bib | ||
321 | +*.run.xml | ||
322 | + | ||
323 | +## Build tool auxiliary files: | ||
45 | *.fdb_latexmk | 324 | *.fdb_latexmk |
325 | +*.synctex | ||
326 | +*.synctex(busy) | ||
46 | *.synctex.gz | 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,17 +761,38 @@ OBJA = wx.NewId() | ||
761 | OBJC = wx.NewId() | 761 | OBJC = wx.NewId() |
762 | OBJF = wx.NewId() | 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 | MTC_PROBE_NAME = "1Probe" | 797 | MTC_PROBE_NAME = "1Probe" |
777 | MTC_REF_NAME = "2Ref" | 798 | MTC_REF_NAME = "2Ref" |
invesalius/data/bases.py
@@ -188,6 +188,13 @@ def object_registration(fiducials, orients, coord_raw, m_change): | @@ -188,6 +188,13 @@ def object_registration(fiducials, orients, coord_raw, m_change): | ||
188 | fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3] | 188 | fids_raw[ic, :] = dco.dynamic_reference_m2(coords[ic, :], coords[3, :])[:3] |
189 | 189 | ||
190 | # compute initial alignment of probe fixed in the object in source frame | 190 | # compute initial alignment of probe fixed in the object in source frame |
191 | + | ||
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 | s0_raw = dco.coordinates_to_transformation_matrix( | 198 | s0_raw = dco.coordinates_to_transformation_matrix( |
192 | position=coords[3, :3], | 199 | position=coords[3, :3], |
193 | orientation=coords[3, 3:], | 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,6 +149,7 @@ def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)): | ||
149 | # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html | 149 | # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html |
150 | def gdcm_to_numpy(image, apply_intercep_scale=True): | 150 | def gdcm_to_numpy(image, apply_intercep_scale=True): |
151 | map_gdcm_np = { | 151 | map_gdcm_np = { |
152 | + gdcm.PixelFormat.SINGLEBIT: np.uint8, | ||
152 | gdcm.PixelFormat.UINT8: np.uint8, | 153 | gdcm.PixelFormat.UINT8: np.uint8, |
153 | gdcm.PixelFormat.INT8: np.int8, | 154 | gdcm.PixelFormat.INT8: np.int8, |
154 | gdcm.PixelFormat.UINT12: np.uint16, | 155 | gdcm.PixelFormat.UINT12: np.uint16, |
@@ -177,6 +178,8 @@ def gdcm_to_numpy(image, apply_intercep_scale=True): | @@ -177,6 +178,8 @@ def gdcm_to_numpy(image, apply_intercep_scale=True): | ||
177 | np_array = np.frombuffer( | 178 | np_array = np.frombuffer( |
178 | gdcm_array.encode("utf-8", errors="surrogateescape"), dtype=dtype | 179 | gdcm_array.encode("utf-8", errors="surrogateescape"), dtype=dtype |
179 | ) | 180 | ) |
181 | + if pf.GetScalarType() == gdcm.PixelFormat.SINGLEBIT: | ||
182 | + np_array = np.unpackbits(np_array) | ||
180 | np_array.shape = shape | 183 | np_array.shape = shape |
181 | np_array = np_array.squeeze() | 184 | np_array = np_array.squeeze() |
182 | 185 |
invesalius/gui/dialogs.py
@@ -3304,8 +3304,9 @@ class MaskDensityDialog(wx.Dialog): | @@ -3304,8 +3304,9 @@ class MaskDensityDialog(wx.Dialog): | ||
3304 | 3304 | ||
3305 | class ObjectCalibrationDialog(wx.Dialog): | 3305 | class ObjectCalibrationDialog(wx.Dialog): |
3306 | 3306 | ||
3307 | - def __init__(self, tracker): | 3307 | + def __init__(self, tracker, pedal_connection): |
3308 | self.tracker = tracker | 3308 | self.tracker = tracker |
3309 | + self.pedal_connection = pedal_connection | ||
3309 | 3310 | ||
3310 | self.trk_init, self.tracker_id = tracker.GetTrackerInfo() | 3311 | self.trk_init, self.tracker_id = tracker.GetTrackerInfo() |
3311 | 3312 | ||
@@ -3313,6 +3314,7 @@ class ObjectCalibrationDialog(wx.Dialog): | @@ -3313,6 +3314,7 @@ class ObjectCalibrationDialog(wx.Dialog): | ||
3313 | self.obj_name = None | 3314 | self.obj_name = None |
3314 | self.polydata = None | 3315 | self.polydata = None |
3315 | self.use_default_object = False | 3316 | self.use_default_object = False |
3317 | + self.object_fiducial_being_set = None | ||
3316 | 3318 | ||
3317 | self.obj_fiducials = np.full([5, 3], np.nan) | 3319 | self.obj_fiducials = np.full([5, 3], np.nan) |
3318 | self.obj_orients = np.full([5, 3], np.nan) | 3320 | self.obj_orients = np.full([5, 3], np.nan) |
@@ -3323,6 +3325,11 @@ class ObjectCalibrationDialog(wx.Dialog): | @@ -3323,6 +3325,11 @@ class ObjectCalibrationDialog(wx.Dialog): | ||
3323 | self._init_gui() | 3325 | self._init_gui() |
3324 | self.LoadObject() | 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 | def _init_gui(self): | 3333 | def _init_gui(self): |
3327 | self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) | 3334 | self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) |
3328 | self.interactor.Enable(1) | 3335 | self.interactor.Enable(1) |
@@ -3373,15 +3380,17 @@ class ObjectCalibrationDialog(wx.Dialog): | @@ -3373,15 +3380,17 @@ class ObjectCalibrationDialog(wx.Dialog): | ||
3373 | choice_sensor]) | 3380 | choice_sensor]) |
3374 | 3381 | ||
3375 | # Push buttons for object fiducials | 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 | for m in range(0, 5): | 3395 | for m in range(0, 5): |
3387 | for n in range(0, 3): | 3396 | for n in range(0, 3): |
@@ -3525,13 +3534,42 @@ class ObjectCalibrationDialog(wx.Dialog): | @@ -3525,13 +3534,42 @@ class ObjectCalibrationDialog(wx.Dialog): | ||
3525 | self.ren.AddActor(ball_actor) | 3534 | self.ren.AddActor(ball_actor) |
3526 | return ball_actor, tactor | 3535 | return ball_actor, tactor |
3527 | 3536 | ||
3528 | - def OnGetObjectFiducials(self, evt): | 3537 | + def OnObjectFiducialButton(self, index, evt, ctrl): |
3529 | if not self.tracker.IsTrackerInitialized(): | 3538 | if not self.tracker.IsTrackerInitialized(): |
3530 | ShowNavigationTrackerWarning(0, 'choose') | 3539 | ShowNavigationTrackerWarning(0, 'choose') |
3531 | return | 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 | coord, coord_raw = self.tracker.GetTrackerCoordinates( | 3573 | coord, coord_raw = self.tracker.GetTrackerCoordinates( |
3536 | # XXX: Always use static reference mode when getting the coordinates. This is what the | 3574 | # XXX: Always use static reference mode when getting the coordinates. This is what the |
3537 | # code did previously, as well. At some point, it should probably be thought through | 3575 | # code did previously, as well. At some point, it should probably be thought through |
@@ -3549,22 +3587,22 @@ class ObjectCalibrationDialog(wx.Dialog): | @@ -3549,22 +3587,22 @@ class ObjectCalibrationDialog(wx.Dialog): | ||
3549 | # mode" principle above, but it's hard to come up with a simple change to increase the consistency | 3587 | # mode" principle above, but it's hard to come up with a simple change to increase the consistency |
3550 | # and not change the function to the point of potentially breaking it.) | 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 | coord = coord_raw[self.obj_ref_id, :] | 3591 | coord = coord_raw[self.obj_ref_id, :] |
3554 | coord[2] = -coord[2] | 3592 | coord[2] = -coord[2] |
3555 | 3593 | ||
3556 | - if btn_id == 3: | 3594 | + if fiducial_index == 3: |
3557 | coord = np.zeros([6,]) | 3595 | coord = np.zeros([6,]) |
3558 | 3596 | ||
3559 | # Update text controls with tracker coordinates | 3597 | # Update text controls with tracker coordinates |
3560 | if coord is not None or np.sum(coord) != 0.0: | 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 | self.Refresh() | 3606 | self.Refresh() |
3569 | else: | 3607 | else: |
3570 | ShowNavigationTrackerWarning(0, 'choose') | 3608 | ShowNavigationTrackerWarning(0, 'choose') |
invesalius/gui/task_navigator.py
@@ -18,12 +18,9 @@ | @@ -18,12 +18,9 @@ | ||
18 | #-------------------------------------------------------------------------- | 18 | #-------------------------------------------------------------------------- |
19 | 19 | ||
20 | from functools import partial | 20 | from functools import partial |
21 | +import itertools | ||
21 | import csv | 22 | import csv |
22 | -import os | ||
23 | -import queue | ||
24 | -import sys | ||
25 | import time | 23 | import time |
26 | -import threading | ||
27 | 24 | ||
28 | import nibabel as nb | 25 | import nibabel as nb |
29 | import numpy as np | 26 | import numpy as np |
@@ -35,10 +32,8 @@ except ImportError: | @@ -35,10 +32,8 @@ except ImportError: | ||
35 | import wx | 32 | import wx |
36 | 33 | ||
37 | try: | 34 | try: |
38 | - import wx.lib.agw.hyperlink as hl | ||
39 | import wx.lib.agw.foldpanelbar as fpb | 35 | import wx.lib.agw.foldpanelbar as fpb |
40 | except ImportError: | 36 | except ImportError: |
41 | - import wx.lib.hyperlink as hl | ||
42 | import wx.lib.foldpanelbar as fpb | 37 | import wx.lib.foldpanelbar as fpb |
43 | 38 | ||
44 | import wx.lib.colourselect as csel | 39 | import wx.lib.colourselect as csel |
@@ -47,25 +42,22 @@ from invesalius.pubsub import pub as Publisher | @@ -47,25 +42,22 @@ from invesalius.pubsub import pub as Publisher | ||
47 | from time import sleep | 42 | from time import sleep |
48 | 43 | ||
49 | import invesalius.constants as const | 44 | import invesalius.constants as const |
50 | -import invesalius.data.bases as db | ||
51 | 45 | ||
52 | if has_trekker: | 46 | if has_trekker: |
53 | import invesalius.data.brainmesh_handler as brain | 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 | import invesalius.data.imagedata_utils as imagedata_utils | 49 | import invesalius.data.imagedata_utils as imagedata_utils |
58 | -import invesalius.data.serial_port_connection as spc | ||
59 | import invesalius.data.slice_ as sl | 50 | import invesalius.data.slice_ as sl |
60 | -import invesalius.data.trackers as dt | ||
61 | import invesalius.data.tractography as dti | 51 | import invesalius.data.tractography as dti |
62 | -import invesalius.data.transformations as tr | ||
63 | import invesalius.data.record_coords as rec | 52 | import invesalius.data.record_coords as rec |
64 | import invesalius.data.vtk_utils as vtk_utils | 53 | import invesalius.data.vtk_utils as vtk_utils |
65 | import invesalius.gui.dialogs as dlg | 54 | import invesalius.gui.dialogs as dlg |
66 | import invesalius.project as prj | 55 | import invesalius.project as prj |
67 | from invesalius import utils | 56 | from invesalius import utils |
68 | from invesalius.gui import utils as gui_utils | 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 | HAS_PEDAL_CONNECTION = True | 62 | HAS_PEDAL_CONNECTION = True |
71 | try: | 63 | try: |
@@ -161,9 +153,10 @@ class InnerFoldPanel(wx.Panel): | @@ -161,9 +153,10 @@ class InnerFoldPanel(wx.Panel): | ||
161 | fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, | 153 | fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, |
162 | (10, 310), 0, fpb.FPB_SINGLE_FOLD) | 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 | tracker = Tracker() | 158 | tracker = Tracker() |
159 | + pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None | ||
167 | 160 | ||
168 | # Fold panel style | 161 | # Fold panel style |
169 | style = fpb.CaptionBarStyle() | 162 | style = fpb.CaptionBarStyle() |
@@ -173,7 +166,7 @@ class InnerFoldPanel(wx.Panel): | @@ -173,7 +166,7 @@ class InnerFoldPanel(wx.Panel): | ||
173 | 166 | ||
174 | # Fold 1 - Navigation panel | 167 | # Fold 1 - Navigation panel |
175 | item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) | 168 | item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) |
176 | - ntw = NeuronavigationPanel(item, tracker) | 169 | + ntw = NeuronavigationPanel(item, tracker, pedal_connection) |
177 | 170 | ||
178 | fold_panel.ApplyCaptionStyle(item, style) | 171 | fold_panel.ApplyCaptionStyle(item, style) |
179 | fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, | 172 | fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, |
@@ -182,7 +175,7 @@ class InnerFoldPanel(wx.Panel): | @@ -182,7 +175,7 @@ class InnerFoldPanel(wx.Panel): | ||
182 | 175 | ||
183 | # Fold 2 - Object registration panel | 176 | # Fold 2 - Object registration panel |
184 | item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) | 177 | item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) |
185 | - otw = ObjectRegistrationPanel(item, tracker) | 178 | + otw = ObjectRegistrationPanel(item, tracker, pedal_connection) |
186 | 179 | ||
187 | fold_panel.ApplyCaptionStyle(item, style) | 180 | fold_panel.ApplyCaptionStyle(item, style) |
188 | fold_panel.AddFoldPanelWindow(item, otw, spacing=0, | 181 | fold_panel.AddFoldPanelWindow(item, otw, spacing=0, |
@@ -315,390 +308,8 @@ class InnerFoldPanel(wx.Panel): | @@ -315,390 +308,8 @@ class InnerFoldPanel(wx.Panel): | ||
315 | Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue()) | 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 | class NeuronavigationPanel(wx.Panel): | 311 | class NeuronavigationPanel(wx.Panel): |
701 | - def __init__(self, parent, tracker): | 312 | + def __init__(self, parent, tracker, pedal_connection): |
702 | wx.Panel.__init__(self, parent) | 313 | wx.Panel.__init__(self, parent) |
703 | try: | 314 | try: |
704 | default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) | 315 | default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) |
@@ -711,15 +322,16 @@ class NeuronavigationPanel(wx.Panel): | @@ -711,15 +322,16 @@ class NeuronavigationPanel(wx.Panel): | ||
711 | self.__bind_events() | 322 | self.__bind_events() |
712 | 323 | ||
713 | # Initialize global variables | 324 | # Initialize global variables |
714 | - self.pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None | 325 | + self.pedal_connection = pedal_connection |
715 | self.navigation = Navigation( | 326 | self.navigation = Navigation( |
716 | - pedal_connection=self.pedal_connection, | 327 | + pedal_connection=pedal_connection, |
717 | ) | 328 | ) |
718 | self.icp = ICP() | 329 | self.icp = ICP() |
719 | self.tracker = tracker | 330 | self.tracker = tracker |
720 | 331 | ||
721 | self.nav_status = False | 332 | self.nav_status = False |
722 | self.tracker_fiducial_being_set = None | 333 | self.tracker_fiducial_being_set = None |
334 | + self.current_coord = 0, 0, 0 | ||
723 | 335 | ||
724 | # Initialize list of buttons and numctrls for wx objects | 336 | # Initialize list of buttons and numctrls for wx objects |
725 | self.btns_set_fiducial = [None, None, None, None, None, None] | 337 | self.btns_set_fiducial = [None, None, None, None, None, None] |
@@ -776,7 +388,7 @@ class NeuronavigationPanel(wx.Panel): | @@ -776,7 +388,7 @@ class NeuronavigationPanel(wx.Panel): | ||
776 | txt_fre = wx.StaticText(self, -1, _('FRE:')) | 388 | txt_fre = wx.StaticText(self, -1, _('FRE:')) |
777 | txt_icp = wx.StaticText(self, -1, _('Refine:')) | 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 | txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) | 392 | txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) |
781 | else: | 393 | else: |
782 | txt_pedal_pressed = None | 394 | txt_pedal_pressed = None |
@@ -805,14 +417,14 @@ class NeuronavigationPanel(wx.Panel): | @@ -805,14 +417,14 @@ class NeuronavigationPanel(wx.Panel): | ||
805 | self.checkbox_icp = checkbox_icp | 417 | self.checkbox_icp = checkbox_icp |
806 | 418 | ||
807 | # An indicator for pedal trigger | 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 | tooltip = wx.ToolTip(_(u"Is the pedal pressed")) | 421 | tooltip = wx.ToolTip(_(u"Is the pedal pressed")) |
810 | checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) | 422 | checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) |
811 | checkbox_pedal_pressed.SetValue(False) | 423 | checkbox_pedal_pressed.SetValue(False) |
812 | checkbox_pedal_pressed.Enable(False) | 424 | checkbox_pedal_pressed.Enable(False) |
813 | checkbox_pedal_pressed.SetToolTip(tooltip) | 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 | self.checkbox_pedal_pressed = checkbox_pedal_pressed | 429 | self.checkbox_pedal_pressed = checkbox_pedal_pressed |
818 | else: | 430 | else: |
@@ -846,7 +458,7 @@ class NeuronavigationPanel(wx.Panel): | @@ -846,7 +458,7 @@ class NeuronavigationPanel(wx.Panel): | ||
846 | (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) | 458 | (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) |
847 | 459 | ||
848 | pedal_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) | 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 | pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), | 462 | pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), |
851 | (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) | 463 | (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) |
852 | 464 | ||
@@ -982,7 +594,7 @@ class NeuronavigationPanel(wx.Panel): | @@ -982,7 +594,7 @@ class NeuronavigationPanel(wx.Panel): | ||
982 | 594 | ||
983 | def UpdateImageCoordinates(self, position): | 595 | def UpdateImageCoordinates(self, position): |
984 | # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication. | 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 | for m in [0, 1, 2]: | 599 | for m in [0, 1, 2]: |
988 | if not self.btns_set_fiducial[m].GetValue(): | 600 | if not self.btns_set_fiducial[m].GetValue(): |
@@ -1039,7 +651,7 @@ class NeuronavigationPanel(wx.Panel): | @@ -1039,7 +651,7 @@ class NeuronavigationPanel(wx.Panel): | ||
1039 | fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] | 651 | fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] |
1040 | 652 | ||
1041 | # XXX: This is still a bit hard to read, could be cleaned up. | 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 | if self.btns_set_fiducial[n].GetValue(): | 656 | if self.btns_set_fiducial[n].GetValue(): |
1045 | coord = self.numctrls_fiducial[n][0].GetValue(),\ | 657 | coord = self.numctrls_fiducial[n][0].GetValue(),\ |
@@ -1053,13 +665,13 @@ class NeuronavigationPanel(wx.Panel): | @@ -1053,13 +665,13 @@ class NeuronavigationPanel(wx.Panel): | ||
1053 | seed = 3 * [0.] | 665 | seed = 3 * [0.] |
1054 | 666 | ||
1055 | Publisher.sendMessage('Create marker', coord=coord, colour=colour, size=size, | 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 | else: | 669 | else: |
1058 | for m in [0, 1, 2]: | 670 | for m in [0, 1, 2]: |
1059 | self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) | 671 | self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) |
1060 | 672 | ||
1061 | Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, coord=np.nan) | 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 | def OnTrackerFiducials(self, n, evt, ctrl): | 676 | def OnTrackerFiducials(self, n, evt, ctrl): |
1065 | 677 | ||
@@ -1074,14 +686,16 @@ class NeuronavigationPanel(wx.Panel): | @@ -1074,14 +686,16 @@ class NeuronavigationPanel(wx.Panel): | ||
1074 | def set_fiducial_callback(): | 686 | def set_fiducial_callback(): |
1075 | fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name'] | 687 | fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name'] |
1076 | Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) | 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 | ctrl.SetValue(False) | 692 | ctrl.SetValue(False) |
1080 | self.tracker_fiducial_being_set = None | 693 | self.tracker_fiducial_being_set = None |
1081 | 694 | ||
1082 | if ctrl.GetValue(): | 695 | if ctrl.GetValue(): |
1083 | self.tracker_fiducial_being_set = n | 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 | else: | 699 | else: |
1086 | set_fiducial_callback() | 700 | set_fiducial_callback() |
1087 | 701 | ||
@@ -1195,7 +809,7 @@ class NeuronavigationPanel(wx.Panel): | @@ -1195,7 +809,7 @@ class NeuronavigationPanel(wx.Panel): | ||
1195 | 809 | ||
1196 | 810 | ||
1197 | class ObjectRegistrationPanel(wx.Panel): | 811 | class ObjectRegistrationPanel(wx.Panel): |
1198 | - def __init__(self, parent, tracker): | 812 | + def __init__(self, parent, tracker, pedal_connection): |
1199 | wx.Panel.__init__(self, parent) | 813 | wx.Panel.__init__(self, parent) |
1200 | try: | 814 | try: |
1201 | default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) | 815 | default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) |
@@ -1206,6 +820,7 @@ class ObjectRegistrationPanel(wx.Panel): | @@ -1206,6 +820,7 @@ class ObjectRegistrationPanel(wx.Panel): | ||
1206 | self.coil_list = const.COIL | 820 | self.coil_list = const.COIL |
1207 | 821 | ||
1208 | self.tracker = tracker | 822 | self.tracker = tracker |
823 | + self.pedal_connection = pedal_connection | ||
1209 | 824 | ||
1210 | self.nav_prop = None | 825 | self.nav_prop = None |
1211 | self.obj_fiducials = None | 826 | self.obj_fiducials = None |
@@ -1368,7 +983,7 @@ class ObjectRegistrationPanel(wx.Panel): | @@ -1368,7 +983,7 @@ class ObjectRegistrationPanel(wx.Panel): | ||
1368 | def OnLinkCreate(self, event=None): | 983 | def OnLinkCreate(self, event=None): |
1369 | 984 | ||
1370 | if self.tracker.IsTrackerInitialized(): | 985 | if self.tracker.IsTrackerInitialized(): |
1371 | - dialog = dlg.ObjectCalibrationDialog(self.tracker) | 986 | + dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection) |
1372 | try: | 987 | try: |
1373 | if dialog.ShowModal() == wx.ID_OK: | 988 | if dialog.ShowModal() == wx.ID_OK: |
1374 | self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue() | 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,7 +1094,6 @@ class MarkersPanel(wx.Panel): | ||
1479 | self.current_angle = 0, 0, 0 | 1094 | self.current_angle = 0, 0, 0 |
1480 | self.current_seed = 0, 0, 0 | 1095 | self.current_seed = 0, 0, 0 |
1481 | self.list_coord = [] | 1096 | self.list_coord = [] |
1482 | - self.marker_ind = 0 | ||
1483 | self.tgt_flag = self.tgt_index = None | 1097 | self.tgt_flag = self.tgt_index = None |
1484 | self.nav_status = False | 1098 | self.nav_status = False |
1485 | 1099 | ||
@@ -1525,7 +1139,7 @@ class MarkersPanel(wx.Panel): | @@ -1525,7 +1139,7 @@ class MarkersPanel(wx.Panel): | ||
1525 | 1139 | ||
1526 | # Buttons to delete or remove markers | 1140 | # Buttons to delete or remove markers |
1527 | btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23)) | 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 | btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23)) | 1144 | btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23)) |
1531 | btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers) | 1145 | btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers) |
@@ -1564,7 +1178,7 @@ class MarkersPanel(wx.Panel): | @@ -1564,7 +1178,7 @@ class MarkersPanel(wx.Panel): | ||
1564 | def __bind_events(self): | 1178 | def __bind_events(self): |
1565 | # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points') | 1179 | # Publisher.subscribe(self.UpdateCurrentCoord, 'Co-registered points') |
1566 | Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point') | 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 | Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') | 1182 | Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') |
1569 | Publisher.subscribe(self.CreateMarker, 'Create marker') | 1183 | Publisher.subscribe(self.CreateMarker, 'Create marker') |
1570 | Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') | 1184 | Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') |
@@ -1680,7 +1294,6 @@ class MarkersPanel(wx.Panel): | @@ -1680,7 +1294,6 @@ class MarkersPanel(wx.Panel): | ||
1680 | 1294 | ||
1681 | if result == wx.ID_OK: | 1295 | if result == wx.ID_OK: |
1682 | self.list_coord = [] | 1296 | self.list_coord = [] |
1683 | - self.marker_ind = 0 | ||
1684 | Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount()) | 1297 | Publisher.sendMessage('Remove all markers', indexes=self.lc.GetItemCount()) |
1685 | self.lc.DeleteAllItems() | 1298 | self.lc.DeleteAllItems() |
1686 | Publisher.sendMessage('Stop Blink Marker', index='DeleteAll') | 1299 | Publisher.sendMessage('Stop Blink Marker', index='DeleteAll') |
@@ -1691,46 +1304,47 @@ class MarkersPanel(wx.Panel): | @@ -1691,46 +1304,47 @@ class MarkersPanel(wx.Panel): | ||
1691 | if not hasattr(evt, 'data'): | 1304 | if not hasattr(evt, 'data'): |
1692 | wx.MessageBox(_("Target deleted."), _("InVesalius 3")) | 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 | # Pubsub is used for fiducial handle and button click for all others | 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 | for id_n in range(self.lc.GetItemCount()): | 1316 | for id_n in range(self.lc.GetItemCount()): |
1701 | item = self.lc.GetItem(id_n, 4) | 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 | if index: | 1326 | if index: |
1715 | if self.tgt_flag and self.tgt_index == index[0]: | 1327 | if self.tgt_flag and self.tgt_index == index[0]: |
1716 | self.tgt_flag = self.tgt_index = None | 1328 | self.tgt_flag = self.tgt_index = None |
1717 | Publisher.sendMessage('Disable or enable coil tracker', status=False) | 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 | else: | 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 | for i in reversed(index): | 1340 | for i in reversed(index): |
1726 | del self.list_coord[i] | 1341 | del self.list_coord[i] |
1727 | self.lc.DeleteItem(i) | 1342 | self.lc.DeleteItem(i) |
1728 | for n in range(0, self.lc.GetItemCount()): | 1343 | for n in range(0, self.lc.GetItemCount()): |
1729 | self.lc.SetItem(n, 0, str(n+1)) | 1344 | self.lc.SetItem(n, 0, str(n+1)) |
1730 | - self.marker_ind -= 1 | ||
1731 | Publisher.sendMessage('Remove marker', index=index) | 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 | self.CreateMarker() | 1348 | self.CreateMarker() |
1735 | 1349 | ||
1736 | def OnLoadMarkers(self, evt): | 1350 | def OnLoadMarkers(self, evt): |
@@ -1792,7 +1406,7 @@ class MarkersPanel(wx.Panel): | @@ -1792,7 +1406,7 @@ class MarkersPanel(wx.Panel): | ||
1792 | marker_id = '*' | 1406 | marker_id = '*' |
1793 | 1407 | ||
1794 | self.CreateMarker(coord=coord, colour=colour, size=size, | 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 | # if there are multiple TARGETS will set the last one | 1411 | # if there are multiple TARGETS will set the last one |
1798 | if target: | 1412 | if target: |
@@ -1847,7 +1461,7 @@ class MarkersPanel(wx.Panel): | @@ -1847,7 +1461,7 @@ class MarkersPanel(wx.Panel): | ||
1847 | target_id = line[20] | 1461 | target_id = line[20] |
1848 | 1462 | ||
1849 | self.CreateMarker(coord=coord, colour=colour, size=size, | 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 | # if there are multiple TARGETS will set the last one | 1466 | # if there are multiple TARGETS will set the last one |
1853 | if target: | 1467 | if target: |
@@ -1903,7 +1517,7 @@ class MarkersPanel(wx.Panel): | @@ -1903,7 +1517,7 @@ class MarkersPanel(wx.Panel): | ||
1903 | def OnSelectSize(self, evt, ctrl): | 1517 | def OnSelectSize(self, evt, ctrl): |
1904 | self.marker_size = ctrl.GetValue() | 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 | coord = coord or self.current_coord | 1521 | coord = coord or self.current_coord |
1908 | colour = colour or self.marker_colour | 1522 | colour = colour or self.marker_colour |
1909 | size = size or self.marker_size | 1523 | size = size or self.marker_size |
@@ -1917,9 +1531,7 @@ class MarkersPanel(wx.Panel): | @@ -1917,9 +1531,7 @@ class MarkersPanel(wx.Panel): | ||
1917 | # TODO: Use matrix coordinates and not world coordinates as current method. | 1531 | # TODO: Use matrix coordinates and not world coordinates as current method. |
1918 | # This makes easier for inter-software comprehension. | 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 | # List of lists with coordinates and properties of a marker | 1536 | # List of lists with coordinates and properties of a marker |
1925 | line = [] | 1537 | line = [] |
@@ -1928,7 +1540,7 @@ class MarkersPanel(wx.Panel): | @@ -1928,7 +1540,7 @@ class MarkersPanel(wx.Panel): | ||
1928 | line.extend(orientation_world) | 1540 | line.extend(orientation_world) |
1929 | line.extend(colour) | 1541 | line.extend(colour) |
1930 | line.append(size) | 1542 | line.append(size) |
1931 | - line.append(marker_id) | 1543 | + line.append(label) |
1932 | line.extend(seed) | 1544 | line.extend(seed) |
1933 | line.append(target_id) | 1545 | line.append(target_id) |
1934 | 1546 | ||
@@ -1944,21 +1556,24 @@ class MarkersPanel(wx.Panel): | @@ -1944,21 +1556,24 @@ class MarkersPanel(wx.Panel): | ||
1944 | self.lc.SetItem(num_items, 1, str(round(coord[0], 2))) | 1556 | self.lc.SetItem(num_items, 1, str(round(coord[0], 2))) |
1945 | self.lc.SetItem(num_items, 2, str(round(coord[1], 2))) | 1557 | self.lc.SetItem(num_items, 2, str(round(coord[1], 2))) |
1946 | self.lc.SetItem(num_items, 3, str(round(coord[2], 2))) | 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 | self.lc.EnsureVisible(num_items) | 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 | selection = [] | 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 | return selection | 1574 | return selection |
1961 | 1575 | ||
1576 | + | ||
1962 | class DbsPanel(wx.Panel): | 1577 | class DbsPanel(wx.Panel): |
1963 | def __init__(self, parent): | 1578 | def __init__(self, parent): |
1964 | wx.Panel.__init__(self, parent) | 1579 | wx.Panel.__init__(self, parent) |
@@ -2410,100 +2025,6 @@ class TractographyPanel(wx.Panel): | @@ -2410,100 +2025,6 @@ class TractographyPanel(wx.Panel): | ||
2410 | Publisher.sendMessage('Remove tracts') | 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 | class InputAttributes(object): | 2028 | class InputAttributes(object): |
2508 | # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python | 2029 | # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python |
2509 | def __init__(self, *initial_data, **kwargs): | 2030 | def __init__(self, *initial_data, **kwargs): |
@@ -0,0 +1,80 @@ | @@ -0,0 +1,80 @@ | ||
1 | +#-------------------------------------------------------------------------- | ||
2 | +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas | ||
3 | +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer | ||
4 | +# Homepage: http://www.softwarepublico.gov.br | ||
5 | +# Contact: invesalius@cti.gov.br | ||
6 | +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) | ||
7 | +#-------------------------------------------------------------------------- | ||
8 | +# Este programa e software livre; voce pode redistribui-lo e/ou | ||
9 | +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme | ||
10 | +# publicada pela Free Software Foundation; de acordo com a versao 2 | ||
11 | +# da Licenca. | ||
12 | +# | ||
13 | +# Este programa eh distribuido na expectativa de ser util, mas SEM | ||
14 | +# QUALQUER GARANTIA; sem mesmo a garantia implicita de | ||
15 | +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM | ||
16 | +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais | ||
17 | +# detalhes. | ||
18 | +#-------------------------------------------------------------------------- | ||
19 | + | ||
20 | +import wx | ||
21 | + | ||
22 | +import invesalius.data.bases as db | ||
23 | +import invesalius.gui.dialogs as dlg | ||
24 | +from invesalius.pubsub import pub as Publisher | ||
25 | + | ||
26 | + | ||
27 | +class ICP(): | ||
28 | + def __init__(self): | ||
29 | + self.use_icp = False | ||
30 | + self.m_icp = None | ||
31 | + self.icp_fre = None | ||
32 | + | ||
33 | + def StartICP(self, navigation, tracker): | ||
34 | + ref_mode_id = navigation.GetReferenceMode() | ||
35 | + | ||
36 | + if not self.use_icp: | ||
37 | + if dlg.ICPcorregistration(navigation.fre): | ||
38 | + Publisher.sendMessage('Stop navigation') | ||
39 | + use_icp, self.m_icp = self.OnICP(navigation, tracker, navigation.m_change) | ||
40 | + if use_icp: | ||
41 | + self.icp_fre = db.calculate_fre(tracker.tracker_fiducials_raw, navigation.all_fiducials, | ||
42 | + ref_mode_id, navigation.m_change, self.m_icp) | ||
43 | + self.SetICP(navigation, use_icp) | ||
44 | + else: | ||
45 | + print("ICP canceled") | ||
46 | + Publisher.sendMessage('Start navigation') | ||
47 | + | ||
48 | + def OnICP(self, navigation, tracker, m_change): | ||
49 | + ref_mode_id = navigation.GetReferenceMode() | ||
50 | + | ||
51 | + dialog = dlg.ICPCorregistrationDialog(nav_prop=(m_change, tracker.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 @@ | @@ -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 @@ | @@ -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 |