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,6 +342,9 @@ def parse_comand_line(): | ||
342 | dest="save_masks", default=True, | 342 | dest="save_masks", default=True, |
343 | help="Make InVesalius not export mask when exporting project.") | 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 | options, args = parser.parse_args() | 348 | options, args = parser.parse_args() |
346 | return options, args | 349 | return options, args |
347 | 350 | ||
@@ -353,6 +356,12 @@ def use_cmd_optargs(options, args): | @@ -353,6 +356,12 @@ def use_cmd_optargs(options, args): | ||
353 | session = ses.Session() | 356 | session = ses.Session() |
354 | session.debug = 1 | 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 | # If import DICOM argument... | 365 | # If import DICOM argument... |
357 | if options.dicom_dir: | 366 | if options.dicom_dir: |
358 | import_dir = options.dicom_dir | 367 | import_dir = options.dicom_dir |
invesalius/gui/task_navigator.py
@@ -64,6 +64,12 @@ import invesalius.gui.dialogs as dlg | @@ -64,6 +64,12 @@ import invesalius.gui.dialogs as dlg | ||
64 | import invesalius.project as prj | 64 | import invesalius.project as prj |
65 | from invesalius import utils | 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 | BTN_NEW = wx.NewId() | 73 | BTN_NEW = wx.NewId() |
68 | BTN_IMPORT_LOCAL = wx.NewId() | 74 | BTN_IMPORT_LOCAL = wx.NewId() |
69 | 75 | ||
@@ -308,6 +314,7 @@ class NeuronavigationPanel(wx.Panel): | @@ -308,6 +314,7 @@ class NeuronavigationPanel(wx.Panel): | ||
308 | self.__bind_events() | 314 | self.__bind_events() |
309 | 315 | ||
310 | # Initialize global variables | 316 | # Initialize global variables |
317 | + self.pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None | ||
311 | self.fiducials = np.full([6, 3], np.nan) | 318 | self.fiducials = np.full([6, 3], np.nan) |
312 | self.fiducials_raw = np.zeros((6, 6)) | 319 | self.fiducials_raw = np.zeros((6, 6)) |
313 | self.correg = None | 320 | self.correg = None |
@@ -394,6 +401,11 @@ class NeuronavigationPanel(wx.Panel): | @@ -394,6 +401,11 @@ class NeuronavigationPanel(wx.Panel): | ||
394 | txt_fre = wx.StaticText(self, -1, _('FRE:')) | 401 | txt_fre = wx.StaticText(self, -1, _('FRE:')) |
395 | txt_icp = wx.StaticText(self, -1, _('Refine:')) | 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 | # Fiducial registration error text box | 409 | # Fiducial registration error text box |
398 | tooltip = wx.ToolTip(_("Fiducial registration error")) | 410 | tooltip = wx.ToolTip(_("Fiducial registration error")) |
399 | txtctrl_fre = wx.TextCtrl(self, value="", size=wx.Size(60, -1), style=wx.TE_CENTRE) | 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,6 +429,23 @@ class NeuronavigationPanel(wx.Panel): | ||
417 | checkicp.SetToolTip(tooltip) | 429 | checkicp.SetToolTip(tooltip) |
418 | self.checkicp = checkicp | 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 | # Image and tracker coordinates number controls | 449 | # Image and tracker coordinates number controls |
421 | for m in range(len(self.btns_coord)): | 450 | for m in range(len(self.btns_coord)): |
422 | for n in range(3): | 451 | for n in range(3): |
@@ -442,9 +471,14 @@ class NeuronavigationPanel(wx.Panel): | @@ -442,9 +471,14 @@ class NeuronavigationPanel(wx.Panel): | ||
442 | (txtctrl_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), | 471 | (txtctrl_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), |
443 | (btn_nav, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), | 472 | (btn_nav, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), |
444 | (txt_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), | 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 | group_sizer.AddGrowableCol(0, 1) | 482 | group_sizer.AddGrowableCol(0, 1) |
449 | group_sizer.AddGrowableRow(0, 1) | 483 | group_sizer.AddGrowableRow(0, 1) |
450 | group_sizer.AddGrowableRow(1, 1) | 484 | group_sizer.AddGrowableRow(1, 1) |
@@ -452,7 +486,8 @@ class NeuronavigationPanel(wx.Panel): | @@ -452,7 +486,8 @@ class NeuronavigationPanel(wx.Panel): | ||
452 | group_sizer.SetFlexibleDirection(wx.BOTH) | 486 | group_sizer.SetFlexibleDirection(wx.BOTH) |
453 | group_sizer.AddMany([(choice_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), | 487 | group_sizer.AddMany([(choice_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), |
454 | (coord_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), | 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 | main_sizer = wx.BoxSizer(wx.HORIZONTAL) | 492 | main_sizer = wx.BoxSizer(wx.HORIZONTAL) |
458 | main_sizer.Add(group_sizer, 1)# wx.ALIGN_CENTER_HORIZONTAL, 10) | 493 | main_sizer.Add(group_sizer, 1)# wx.ALIGN_CENTER_HORIZONTAL, 10) |
@@ -0,0 +1,95 @@ | @@ -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) |