Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved object and tracker fiducial registration UI #786

Merged
merged 14 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions invesalius/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,10 @@
#Colors for errors and positives
RED_COLOR_FLOAT = (0.99, 0.55, 0.38)
GREEN_COLOR_FLOAT = (0.40, 0.76, 0.65)
YELLOW_COLOR_FLOAT = (1.0, 0.77, 0.0)
RED_COLOR_RGB = (252, 141, 98)
GREEN_COLOR_RGB = (102, 194, 165)
YELLOW_COLOR_RGB = (255, 196, 0)

# Volume view angle
VOL_FRONT = wx.NewIdRef()
Expand Down Expand Up @@ -737,6 +739,7 @@
DEFAULT_REF_MODE = DYNAMIC_REF
REF_MODE = [_("Static ref."), _("Dynamic ref.")]
FT_SENSOR_MODE = [_("Sensor 3"), _("Sensor 4")]
TRACKERS_WITH_SENSOR_OPTIONS = [FASTRAK, ISOTRAKII, PATRIOT, DEBUGTRACKRANDOM, DEBUGTRACKAPPROACH]

DEFAULT_COIL = SELECT
COIL = [_("Select coil:"), _("Neurosoft Figure-8"),
Expand All @@ -751,6 +754,7 @@
SET = wx.NewIdRef()

FIDUCIAL_LABELS = ["Left Ear: ", "Right Ear: ", "Nose: "]
FIDUCIAL_REGISTRATION_ORDER = [0, 2, 1]
IMAGE_FIDUCIALS = [
{
'button_id': IR1,
Expand Down Expand Up @@ -808,6 +812,9 @@
OBJA = wx.NewIdRef()
OBJF = wx.NewIdRef()

OBJECT_FIDUCIAL_ANTERIOR = 2
OBJECT_FIDUCIAL_FIXED = 3

OBJECT_FIDUCIALS = [
{
'fiducial_index': 0,
Expand All @@ -822,13 +829,13 @@
'tip': _("Select right object fiducial"),
},
{
'fiducial_index': 2,
'fiducial_index': OBJECT_FIDUCIAL_ANTERIOR,
'button_id': OBJA,
'label': _('Anterior'),
'tip': _("Select anterior object fiducial"),
},
{
'fiducial_index': 3,
'fiducial_index': OBJECT_FIDUCIAL_FIXED,
'button_id': OBJF,
'label': _('Fixed'),
'tip': _("Attach sensor to object"),
Expand Down
165 changes: 99 additions & 66 deletions invesalius/gui/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
import invesalius.data.polydata_utils as pu
from invesalius.gui.widgets.inv_spinctrl import InvSpinCtrl, InvFloatSpinCtrl
from invesalius.gui.widgets.clut_imagedata import CLUTImageDataWidget, EVT_CLUT_NODE_CHANGED
from invesalius.gui.widgets.fiducial_buttons import OrderedFiducialButtons
from invesalius.i18n import tr as _
import numpy as np

Expand Down Expand Up @@ -3449,10 +3450,10 @@ def __init__(self, tracker, pedal_connector, neuronavigation_api):
self.neuronavigation_api = neuronavigation_api

self.tracker_id = tracker.GetTrackerId()
self.show_sensor_options: bool = self.tracker_id in const.TRACKERS_WITH_SENSOR_OPTIONS
self.obj_ref_id = 2
self.coil_path = None
self.polydata = None
self.object_fiducial_being_set = None

self.obj_fiducials = np.full([4, 3], np.nan)
self.obj_orients = np.full([4, 3], np.nan)
Expand All @@ -3461,13 +3462,9 @@ def __init__(self, tracker, pedal_connector, neuronavigation_api):
style=wx.DEFAULT_DIALOG_STYLE | wx.FRAME_FLOAT_ON_PARENT|wx.STAY_ON_TOP)

self._init_gui()
self._init_pedal()
self.InitializeObject()

self.__bind_events()

def __bind_events(self):
pass

def _init_gui(self):
self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize())
self.interactor.Enable(1)
Expand Down Expand Up @@ -3500,48 +3497,59 @@ def _init_gui(self):
choice_sensor.SetSelection(0)
choice_sensor.SetToolTip(tooltip)
choice_sensor.Bind(wx.EVT_COMBOBOX, self.OnChoiceFTSensor)
if self.tracker_id in [const.FASTRAK, const.DEBUGTRACKRANDOM, const.DEBUGTRACKAPPROACH]:
self.choice_sensor = choice_sensor

# Show tracker reference mode and sensor selection for certain trackers only
if self.show_sensor_options:
choice_ref.Show(True)
choice_sensor.Show(True)
else:
choice_ref.Show(False)
choice_sensor.Show(False)
self.choice_sensor = choice_sensor

tooltip = _("Reset all fiducials")
btn_reset = wx.Button(self, -1, _("Reset"), size=wx.Size(90, 30))
btn_reset.SetToolTip(tooltip)
btn_reset.Bind(wx.EVT_BUTTON, self.OnReset)

# Buttons to finish or cancel object registration
tooltip = _(u"Registration done")
# btn_ok = wx.Button(self, -1, _(u"Done"), size=wx.Size(90, 30))
btn_ok = wx.Button(self, wx.ID_OK, _(u"Done"), size=wx.Size(90, 30))
btn_ok.SetToolTip(tooltip)

extra_sizer = wx.FlexGridSizer(rows=3, cols=1, hgap=5, vgap=30)
extra_sizer = wx.FlexGridSizer(cols=1, hgap=5, vgap=10)
extra_sizer.AddMany([choice_ref,
btn_reset,
btn_ok,
choice_sensor])

# Push buttons for object fiducials
for object_fiducial in const.OBJECT_FIDUCIALS:
index = object_fiducial['fiducial_index']
label = object_fiducial['label']
button_id = object_fiducial['button_id']
tip = object_fiducial['tip']
# Buttons for object fiducials
self.buttons = OrderedFiducialButtons(self, const.OBJECT_FIDUCIALS, self.IsObjectFiducialSet)

ctrl = wx.ToggleButton(self, button_id, label=label, size=wx.Size(60, 23))
ctrl.SetToolTip(tip)
ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnObjectFiducialButton, index, ctrl=ctrl))
for index, btn in enumerate(self.buttons):
btn.Bind(wx.EVT_BUTTON, partial(self.OnObjectFiducialButton, index))

self.btns_coord[index] = ctrl
self.buttons.FocusNext()

# Display fiducial coordinates
for m in range(0, 4):
for n in range(0, 3):
self.txt_coord[m].append(wx.StaticText(self, -1, label='-',
style=wx.ALIGN_RIGHT, size=wx.Size(40, 23)))

coord_sizer = wx.GridBagSizer(hgap=20, vgap=5)

for m in range(0, 4):
coord_sizer.Add(self.btns_coord[m], pos=wx.GBPosition(m, 0))
for m, button in enumerate(self.buttons):
coord_sizer.Add(button, pos=wx.GBPosition(m, 0))
for n in range(0, 3):
coord_sizer.Add(self.txt_coord[m][n], pos=wx.GBPosition(m, n + 1), flag=wx.TOP, border=5)

# Hide "Fixed fiducial" for trackers other than Polhemus
if not self.show_sensor_options:
self.buttons[const.OBJECT_FIDUCIAL_FIXED].Hide()
for coord in self.txt_coord[const.OBJECT_FIDUCIAL_FIXED]:
coord.Hide()

group_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=50, vgap=5)
group_sizer.AddMany([(coord_sizer, 0, wx.LEFT, 20),
(extra_sizer, 0, wx.LEFT, 10)])
Expand All @@ -3554,6 +3562,15 @@ def _init_gui(self):
self.SetSizer(main_sizer)
main_sizer.Fit(self)

def _init_pedal(self):
def set_fiducial_callback(state):
index = self.buttons.focused_index
if state and index is not None:
self.SetObjectFiducial(index)

self.pedal_connector.add_callback('fiducial', set_fiducial_callback, remove_when_released=False, panel=self)
self.Bind(wx.EVT_BUTTON, self.OnOk)

def ObjectImportDialog(self):
msg = _("Would like to use InVesalius default object?")
if sys.platform == 'darwin':
Expand Down Expand Up @@ -3601,6 +3618,16 @@ def ShowObject(self, polydata):
self.ball_actors[1], self.text_actors[1] = self.OnCreateObjectText('Right', (0,-55,0))
self.ball_actors[2], self.text_actors[2] = self.OnCreateObjectText('Anterior', (23,0,0))

# Match actor colors with fiducial buttons
def set_actor_colors(n, color_float):
if n != const.OBJECT_FIDUCIAL_FIXED:
self.ball_actors[n].GetProperty().SetColor(color_float)
self.text_actors[n].GetProperty().SetColor(color_float)
self.Refresh()

self.buttons.set_actor_colors = set_actor_colors
self.buttons.Update()

self.ren.AddActor(obj_actor)
self.ren.ResetCamera()

Expand Down Expand Up @@ -3654,7 +3681,7 @@ def OnCreateObjectText(self, name, coord):
ball_actor = vtkActor()
ball_actor.SetMapper(mapper)
ball_actor.SetPosition(coord)
ball_actor.GetProperty().SetColor(1, 0, 0)
ball_actor.GetProperty().SetColor(const.RED_COLOR_FLOAT)

textSource = vtkVectorText()
textSource.SetText(name)
Expand All @@ -3663,7 +3690,7 @@ def OnCreateObjectText(self, name, coord):
mapper.SetInputConnection(textSource.GetOutputPort())
tactor = vtkFollower()
tactor.SetMapper(mapper)
tactor.GetProperty().SetColor(1.0, 0.0, 0.0)
tactor.GetProperty().SetColor(const.RED_COLOR_FLOAT)
tactor.SetScale(5)
ball_position = ball_actor.GetPosition()
tactor.SetPosition(ball_position[0]+5, ball_position[1]+5, ball_position[2]+10)
Expand All @@ -3672,42 +3699,25 @@ def OnCreateObjectText(self, name, coord):
self.ren.AddActor(ball_actor)
return ball_actor, tactor

def OnObjectFiducialButton(self, index, evt, ctrl):
if not self.tracker.IsTrackerInitialized():
ShowNavigationTrackerWarning(0, 'choose')
return

# TODO: The code below until the end of the function is essentially copy-paste from
# OnTrackerFiducials function in NeuronavigationPanel class. Probably the easiest
# way to deduplicate this would be to create a Fiducial class, which would contain
# this code just once.
#

# Do not allow several object fiducials to be set at the same time.
if self.object_fiducial_being_set is not None and self.object_fiducial_being_set != index:
ctrl.SetValue(False)
return

# Called when the button for setting the object fiducial is enabled and either pedal is pressed
# or the button is pressed again.
#
def set_fiducial_callback(state):
if state:
self.SetObjectFiducial(index)
Publisher.sendMessage('Set object fiducial', fiducial_index=index)

ctrl.SetValue(False)
self.object_fiducial_being_set = None
def IsObjectFiducialSet(self, fiducial_index):
fiducial = self.obj_fiducials[fiducial_index]
return not np.isnan(fiducial).any()

if ctrl.GetValue():
self.object_fiducial_being_set = index
self.pedal_connector.add_callback('fiducial', set_fiducial_callback, remove_when_released=True, panel=self)
def OnObjectFiducialButton(self, index, evt):
button = self.buttons[index]

if button is self.buttons.focused:
self.SetObjectFiducial(index)
elif self.IsObjectFiducialSet(index):
self.ResetObjectFiducial(index)
else:
set_fiducial_callback(True)
self.pedal_connector.remove_callback('fiducial', panel=self)
self.buttons.Focus(index)

def SetObjectFiducial(self, fiducial_index):
if not self.tracker.IsTrackerInitialized():
ShowNavigationTrackerWarning(0, 'choose')
return

marker_visibilities, coord, coord_raw = self.tracker.GetTrackerCoordinates(
# XXX: Always use static reference mode when getting the coordinates. This is what the
# code did previously, as well. At some point, it should probably be thought through
Expand Down Expand Up @@ -3736,24 +3746,43 @@ def SetObjectFiducial(self, fiducial_index):
# mode" principle above, but it's hard to come up with a simple change to increase the consistency
# and not change the function to the point of potentially breaking it.)
#
if self.obj_ref_id and fiducial_index == 3:
if self.obj_ref_id and fiducial_index == const.OBJECT_FIDUCIAL_FIXED:
coord = coord_raw[self.obj_ref_id, :]
else:
coord = coord_raw[0, :]

# Update text controls with tracker coordinates
Publisher.sendMessage('Set object fiducial', fiducial_index=fiducial_index)

# Update buttons and text controls with tracker coordinates
if coord is not None or np.sum(coord) != 0.0:
self.obj_fiducials[fiducial_index, :] = coord[:3]
self.obj_orients[fiducial_index, :] = coord[3:]
self.buttons.SetFocused()
for i in [0, 1, 2]:
self.txt_coord[fiducial_index][i].SetLabel(str(round(coord[i], 1)))
if self.text_actors[fiducial_index]:
self.text_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0)
self.ball_actors[fiducial_index].GetProperty().SetColor(0.0, 1.0, 0.0)
self.Refresh()
else:
ShowNavigationTrackerWarning(0, 'choose')

# Collect the "fixed fiducial" at the same time as anterior for trackers other than Polhemus
if fiducial_index == const.OBJECT_FIDUCIAL_ANTERIOR and not self.show_sensor_options:
self.SetObjectFiducial(const.OBJECT_FIDUCIAL_FIXED)

def ResetObjectFiducials(self):
for m in range(0, 4):
self.ResetObjectFiducial(m)
self.buttons.Update()

def ResetObjectFiducial(self, index):
self.obj_fiducials[index, :] = np.full([1, 3], np.nan)
self.obj_orients[index, :] = np.full([1, 3], np.nan)
for coord_index in range(0, 3):
self.txt_coord[index][coord_index].SetLabel('-')
self.buttons.Unset(index)

def OnReset(self, evt):
self.ResetObjectFiducials()

def OnChooseReferenceMode(self, evt):
# When ref mode is changed the tracker coordinates are set to nan
# This is for Polhemus FASTRAK wrapper, where the sensor attached to the object can be the stylus (Static
Expand All @@ -3764,17 +3793,13 @@ def OnChooseReferenceMode(self, evt):

if evt.GetSelection() == 1:
self.obj_ref_id = 2
if self.tracker_id in [const.FASTRAK, const.DEBUGTRACKRANDOM, const.DEBUGTRACKAPPROACH]:
if self.tracker_id in const.TRACKERS_WITH_SENSOR_OPTIONS:
self.choice_sensor.Show(self.obj_ref_id)
else:
self.obj_ref_id = 0
self.choice_sensor.Show(self.obj_ref_id)

for m in range(0, 4):
self.obj_fiducials[m, :] = np.full([1, 3], np.nan)
self.obj_orients[m, :] = np.full([1, 3], np.nan)
for n in range(0, 3):
self.txt_coord[m][n].SetLabel('-')
self.ResetObjectFiducials()

# Used to update choice sensor controls
self.Layout()
Expand All @@ -3788,6 +3813,14 @@ def OnChoiceFTSensor(self, evt):
def GetValue(self):
return self.obj_fiducials, self.obj_orients, self.obj_ref_id, self.coil_path, self.polydata

def OnOk(self, evt):
if evt.GetId() == wx.ID_OK:
# This should always be called when the dialog is closed. Seems to be working correctly.
self.pedal_connector.remove_callback('fiducial', panel=self)

evt.Skip()


class ICPCorregistrationDialog(wx.Dialog):

def __init__(self, navigation, tracker):
Expand Down
4 changes: 4 additions & 0 deletions invesalius/gui/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,10 @@ def OnToggle(self, evt=None, id=None):
else:
Publisher.sendMessage('Disable style', style=id)

# const.STATE_REGISTRATION can be disabled with the same button as const.SLICE_STATE_CROSS
if id == const.SLICE_STATE_CROSS and not state:
Publisher.sendMessage('Stop image registration')

for item in self.enable_items:
state = self.GetToolToggled(item)
if state and (item != id):
Expand Down
Loading