Commit 9d79c88d21c6b09ec296a4ceab1e4a4d12ab1c78

Authored by okahilak
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>
@@ -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)
invesalius/net/pedal_connection.py 0 → 100644
@@ -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)
optional-requirements.txt 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +mido==1.2.10
  2 +python-rtmidi==1.4.9