Commit 9d79c88d21c6b09ec296a4ceab1e4a4d12ab1c78
Committed by
GitHub
1 parent
f2fd855b
Exists in
master
ADD: Optional use of trigger pedal (#297)
* ADD: Optional use of trigger pedal - Show the pedal status in a checkbox in the UI. Add actual functions for the pedal later. - For now, hide the feature behind a feature-flag (--use-pedal) * Review comment: Make MIDI requirements optional Co-authored-by: Olli-Pekka Kahilakoski <olli-pekka.kahilakoski@aalto.fi>
Showing
4 changed files
with
144 additions
and
3 deletions
Show diff stats
app.py
... | ... | @@ -342,6 +342,9 @@ def parse_comand_line(): |
342 | 342 | dest="save_masks", default=True, |
343 | 343 | help="Make InVesalius not export mask when exporting project.") |
344 | 344 | |
345 | + parser.add_option("--use-pedal", action="store_true", dest="use_pedal", | |
346 | + help="Use an external trigger pedal") | |
347 | + | |
345 | 348 | options, args = parser.parse_args() |
346 | 349 | return options, args |
347 | 350 | |
... | ... | @@ -353,6 +356,12 @@ def use_cmd_optargs(options, args): |
353 | 356 | session = ses.Session() |
354 | 357 | session.debug = 1 |
355 | 358 | |
359 | + # If use-pedal argument... | |
360 | + if options.use_pedal: | |
361 | + from invesalius.net.pedal_connection import PedalConnection | |
362 | + | |
363 | + PedalConnection().start() | |
364 | + | |
356 | 365 | # If import DICOM argument... |
357 | 366 | if options.dicom_dir: |
358 | 367 | import_dir = options.dicom_dir | ... | ... |
invesalius/gui/task_navigator.py
... | ... | @@ -64,6 +64,12 @@ import invesalius.gui.dialogs as dlg |
64 | 64 | import invesalius.project as prj |
65 | 65 | from invesalius import utils |
66 | 66 | |
67 | +HAS_PEDAL_CONNECTION = True | |
68 | +try: | |
69 | + from invesalius.net.pedal_connection import PedalConnection | |
70 | +except ImportError: | |
71 | + HAS_PEDAL_CONNECTION = False | |
72 | + | |
67 | 73 | BTN_NEW = wx.NewId() |
68 | 74 | BTN_IMPORT_LOCAL = wx.NewId() |
69 | 75 | |
... | ... | @@ -308,6 +314,7 @@ class NeuronavigationPanel(wx.Panel): |
308 | 314 | self.__bind_events() |
309 | 315 | |
310 | 316 | # Initialize global variables |
317 | + self.pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None | |
311 | 318 | self.fiducials = np.full([6, 3], np.nan) |
312 | 319 | self.fiducials_raw = np.zeros((6, 6)) |
313 | 320 | self.correg = None |
... | ... | @@ -394,6 +401,11 @@ class NeuronavigationPanel(wx.Panel): |
394 | 401 | txt_fre = wx.StaticText(self, -1, _('FRE:')) |
395 | 402 | txt_icp = wx.StaticText(self, -1, _('Refine:')) |
396 | 403 | |
404 | + if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: | |
405 | + txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) | |
406 | + else: | |
407 | + txt_pedal_pressed = None | |
408 | + | |
397 | 409 | # Fiducial registration error text box |
398 | 410 | tooltip = wx.ToolTip(_("Fiducial registration error")) |
399 | 411 | txtctrl_fre = wx.TextCtrl(self, value="", size=wx.Size(60, -1), style=wx.TE_CENTRE) |
... | ... | @@ -417,6 +429,23 @@ class NeuronavigationPanel(wx.Panel): |
417 | 429 | checkicp.SetToolTip(tooltip) |
418 | 430 | self.checkicp = checkicp |
419 | 431 | |
432 | + # An indicator for pedal trigger | |
433 | + if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: | |
434 | + tooltip = wx.ToolTip(_(u"Is the pedal pressed")) | |
435 | + checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) | |
436 | + checkbox_pedal_pressed.SetValue(False) | |
437 | + checkbox_pedal_pressed.Enable(False) | |
438 | + checkbox_pedal_pressed.SetToolTip(tooltip) | |
439 | + | |
440 | + def handle_pedal_value_changed(value): | |
441 | + checkbox_pedal_pressed.SetValue(value) | |
442 | + | |
443 | + self.pedal_connection.set_callback(handle_pedal_value_changed) | |
444 | + | |
445 | + self.checkbox_pedal_pressed = checkbox_pedal_pressed | |
446 | + else: | |
447 | + self.checkbox_pedal_pressed = None | |
448 | + | |
420 | 449 | # Image and tracker coordinates number controls |
421 | 450 | for m in range(len(self.btns_coord)): |
422 | 451 | for n in range(3): |
... | ... | @@ -442,9 +471,14 @@ class NeuronavigationPanel(wx.Panel): |
442 | 471 | (txtctrl_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), |
443 | 472 | (btn_nav, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), |
444 | 473 | (txt_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), |
445 | - (checkicp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) | |
474 | + (checkicp, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)]) | |
475 | + | |
476 | + pedal_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) | |
477 | + if HAS_PEDAL_CONNECTION and self.pedal_connection.in_use: | |
478 | + pedal_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), | |
479 | + (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) | |
446 | 480 | |
447 | - group_sizer = wx.FlexGridSizer(rows=9, cols=1, hgap=5, vgap=5) | |
481 | + group_sizer = wx.FlexGridSizer(rows=10, cols=1, hgap=5, vgap=5) | |
448 | 482 | group_sizer.AddGrowableCol(0, 1) |
449 | 483 | group_sizer.AddGrowableRow(0, 1) |
450 | 484 | group_sizer.AddGrowableRow(1, 1) |
... | ... | @@ -452,7 +486,8 @@ class NeuronavigationPanel(wx.Panel): |
452 | 486 | group_sizer.SetFlexibleDirection(wx.BOTH) |
453 | 487 | group_sizer.AddMany([(choice_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), |
454 | 488 | (coord_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), |
455 | - (nav_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL)]) | |
489 | + (nav_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), | |
490 | + (pedal_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL)]) | |
456 | 491 | |
457 | 492 | main_sizer = wx.BoxSizer(wx.HORIZONTAL) |
458 | 493 | main_sizer.Add(group_sizer, 1)# wx.ALIGN_CENTER_HORIZONTAL, 10) | ... | ... |
... | ... | @@ -0,0 +1,95 @@ |
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 time | |
21 | +from threading import Thread | |
22 | + | |
23 | +import mido | |
24 | + | |
25 | +from invesalius.utils import Singleton | |
26 | + | |
27 | +class PedalConnection(Thread, metaclass=Singleton): | |
28 | + """ | |
29 | + Connect to the trigger pedal via MIDI, and allow setting a callback for the pedal | |
30 | + being pressed or released. | |
31 | + | |
32 | + Started by calling PedalConnection().start() | |
33 | + """ | |
34 | + def __init__(self): | |
35 | + Thread.__init__(self) | |
36 | + self.daemon = True | |
37 | + | |
38 | + self.in_use = False | |
39 | + | |
40 | + self._midi_in = None | |
41 | + self._active_inputs = None | |
42 | + self._callback = None | |
43 | + | |
44 | + def _midi_to_pedal(self, msg): | |
45 | + # TODO: At this stage, interpret all note_on messages as the pedal being pressed, | |
46 | + # and note_off messages as the pedal being released. Later, use the correct | |
47 | + # message types and be more stringent about the messages. | |
48 | + # | |
49 | + if msg.type == 'note_on': | |
50 | + if self._callback is None: | |
51 | + print("Pedal pressed, no callback registered") | |
52 | + else: | |
53 | + self._callback(True) | |
54 | + | |
55 | + elif msg.type == 'note_off': | |
56 | + if self._callback is None: | |
57 | + print("Pedal released, no callback registered") | |
58 | + else: | |
59 | + self._callback(False) | |
60 | + | |
61 | + else: | |
62 | + print("Unknown message type received from MIDI device") | |
63 | + | |
64 | + def _connect_if_disconnected(self): | |
65 | + if self._midi_in is None and len(self._midi_inputs) > 0: | |
66 | + self._active_input = self._midi_inputs[0] | |
67 | + self._midi_in = mido.open_input(self._active_input) | |
68 | + self._midi_in._rt.ignore_types(False, False, False) | |
69 | + self._midi_in.callback = self._midi_to_pedal | |
70 | + | |
71 | + print("Connected to MIDI device") | |
72 | + | |
73 | + def _check_disconnected(self): | |
74 | + if self._midi_in is not None: | |
75 | + if self._active_input not in self._midi_inputs: | |
76 | + self._midi_in = None | |
77 | + | |
78 | + print("Disconnected from MIDI device") | |
79 | + | |
80 | + def _update_midi_inputs(self): | |
81 | + self._midi_inputs = mido.get_input_names() | |
82 | + | |
83 | + def is_connected(self): | |
84 | + return self._midi_in is not None | |
85 | + | |
86 | + def set_callback(self, callback): | |
87 | + self._callback = callback | |
88 | + | |
89 | + def run(self): | |
90 | + self.in_use = True | |
91 | + while True: | |
92 | + self._update_midi_inputs() | |
93 | + self._check_disconnected() | |
94 | + self._connect_if_disconnected() | |
95 | + time.sleep(1.0) | ... | ... |