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

Improvements to loading markers files #821

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
113 changes: 113 additions & 0 deletions invesalius/gui/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import invesalius.data.polydata_utils as pu
import invesalius.data.transformations as tr
import invesalius.data.vtk_utils as vtku
import invesalius.gui.utils as gui_utils
import invesalius.gui.widgets.gradient as grad
import invesalius.session as ses
import invesalius.utils as utils
Expand Down Expand Up @@ -7478,6 +7479,118 @@ def GetPath(self) -> str:
return self.path


def LoadMarkersFile(filename, load_image_fiducials=False, fiducials_only=False):
"""
:param filename: Path to file
:param load_image_fiducials: If True, loads image fiducials from the markers file.
:param fiducials_only: If True, only loads fiducials, ignoring all other markers in the file.
"""
from invesalius.data.markers.marker import Marker
from invesalius.navigation.navigation import NavigationHub

markers = NavigationHub().markers

try:
with open(filename) as file:
magick_line = file.readline()
assert magick_line.startswith(const.MARKER_FILE_MAGICK_STRING)
version = int(magick_line.split("_")[-1])
if version not in const.SUPPORTED_MARKER_FILE_VERSIONS:
wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3"))
return

# Use the first line after the magick_line as the names for dictionary keys.
column_names = file.readline().strip().split("\t")
column_names_parsed = [utils.parse_value(name) for name in column_names]

markers_data = []
for line in file:
values = line.strip().split("\t")
values_parsed = [utils.parse_value(value) for value in values]
marker_data = dict(zip(column_names_parsed, values_parsed))

markers_data.append(marker_data)

# Create markers from the dictionary.
markers_list = []
for data in markers_data:
marker = Marker(version=version)
marker.from_dict(data)

# When loading markers from file, do not set target based on is_target
marker.is_target = False

markers_list.append(marker)
if load_image_fiducials and marker.label in gui_utils.list_fiducial_labels():
Publisher.sendMessage(
"Load image fiducials", label=marker.label, position=marker.position
)
if not fiducials_only:
markers.AddMultiple(markers_list)

except Exception as e:
wx.MessageBox(_("Could not import markers"), _("InVesalius 3"))
utils.debug(str(e))


def ImportMarkers():
"""
Show dialog to load markers from file.
Markers are appended to the end of the current marker list.
"""
from invesalius.navigation.navigation import NavigationHub

markers = NavigationHub().markers

last_directory = ses.Session().GetConfig("last_directory_3d_surface", "")
dialog = FileSelectionDialog(_("Load markers"), last_directory, const.WILDCARD_MARKER_FILES)
overwrite_image_checkbox = wx.CheckBox(dialog, -1, _("Overwrite current image fiducials"))
clear_checkbox = wx.CheckBox(dialog, -1, _("Clear all previous markers"))
options_sizer = wx.StaticBoxSizer(wx.VERTICAL, dialog)
options_sizer.Add(overwrite_image_checkbox, 0, wx.LEFT)
options_sizer.Add(clear_checkbox, 0, wx.LEFT)
dialog.sizer.Add(options_sizer, 0, wx.EXPAND | wx.ALL, 5)
dialog.FitSizers()

if dialog.ShowModal() == wx.ID_OK:
filename = dialog.GetPath()

if clear_checkbox.GetValue():
markers.Clear()

LoadMarkersFile(filename, load_image_fiducials=overwrite_image_checkbox.GetValue())


def ImportImageFiducials():
"""
Show dialog to load image fiducials from markers file
"""
from invesalius.navigation.navigation import NavigationHub

markers = NavigationHub().markers

last_directory = ses.Session().GetConfig("last_directory_3d_surface", "")
dialog = FileSelectionDialog(
_("Load image fiducials from markers file"), last_directory, const.WILDCARD_MARKER_FILES
)
load_markers_checkbox = wx.CheckBox(dialog, -1, _("Load other markers from file"))
clear_checkbox = wx.CheckBox(dialog, -1, _("Clear all previous markers"))
options_sizer = wx.StaticBoxSizer(wx.VERTICAL, dialog)
options_sizer.Add(load_markers_checkbox, 0, wx.LEFT)
options_sizer.Add(clear_checkbox, 0, wx.LEFT)
dialog.sizer.Add(options_sizer, 0, wx.EXPAND | wx.ALL, 5)
dialog.FitSizers()

if dialog.ShowModal() == wx.ID_OK:
filename = dialog.GetPath()

if clear_checkbox.GetValue():
markers.Clear()

load_fiducials_only = not load_markers_checkbox.GetValue()
LoadMarkersFile(filename, load_image_fiducials=True, fiducials_only=load_fiducials_only)


class ProgressBarHandler(wx.ProgressDialog):
def __init__(
self,
Expand Down
170 changes: 20 additions & 150 deletions invesalius/gui/task_navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
# detalhes.
# --------------------------------------------------------------------------
import itertools

import os
import time
from functools import partial
Expand Down Expand Up @@ -45,6 +45,7 @@
import invesalius.constants as const
import invesalius.data.coordinates as dco
import invesalius.gui.dialogs as dlg
import invesalius.gui.utils as gui_utils
import invesalius.project as prj
import invesalius.session as ses
from invesalius import inv_paths, utils
Expand Down Expand Up @@ -428,10 +429,14 @@ def __init__(self, parent, nav_hub):
next_button.Disable()
self.next_button = next_button

load_from_file_button = wx.Button(self, label="Load From File")
load_from_file_button.Bind(wx.EVT_BUTTON, self.OnLoadFromFile)

top_sizer = wx.BoxSizer(wx.HORIZONTAL)
top_sizer.AddMany([(start_button), (reset_button)])
top_sizer.AddMany([start_button, reset_button])

bottom_sizer = wx.BoxSizer(wx.HORIZONTAL)
bottom_sizer.Add(load_from_file_button)
bottom_sizer.Add(next_button)

sizer = wx.GridBagSizer(5, 5)
Expand Down Expand Up @@ -460,7 +465,7 @@ def __init__(self, parent, nav_hub):
[
(top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10),
(sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT, 5),
(bottom_sizer, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP, 30),
(bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT | wx.BOTTOM, 30),
]
)
self.sizer = main_sizer
Expand Down Expand Up @@ -564,6 +569,9 @@ def OnResetImageFiducials(self):
self.start_button.SetValue(False)
self.OnStartRegistration(self.start_button, self.start_button)

def OnLoadFromFile(self, evt):
dlg.ImportImageFiducials()

def StartRegistration(self):
Publisher.sendMessage("Enable style", style=const.STATE_REGISTRATION)
for button in self.btns_set_fiducial:
Expand Down Expand Up @@ -2250,6 +2258,7 @@ def __bind_events(self):
Publisher.subscribe(self._UnsetTarget, "Unset target")
Publisher.subscribe(self._UnsetPointOfInterest, "Unset point of interest")
Publisher.subscribe(self._UpdateMarkerLabel, "Update marker label")
Publisher.subscribe(self._SetMarkersListRendering, "Set markers list rendering")
Publisher.subscribe(self._UpdateMEP, "Update marker mep")

Publisher.subscribe(self.SetBrainTarget, "Set brain targets")
Expand Down Expand Up @@ -2384,13 +2393,6 @@ def _UpdateMEP(self, marker):
# Trigger redraw MEP mapping
Publisher.sendMessage("Redraw MEP mapping")

@staticmethod
def __list_fiducial_labels():
"""Return the list of marker labels denoting fiducials."""
return list(
itertools.chain(*(const.BTNS_IMG_MARKERS[i].values() for i in const.BTNS_IMG_MARKERS))
)

def UpdateCurrentCoord(self, position):
self.current_position = list(position[:3])
self.current_orientation = list(position[3:])
Expand Down Expand Up @@ -3291,7 +3293,7 @@ def OnDeleteAllMarkers(self, evt=None):

def OnDeleteFiducialMarker(self, label):
indexes = []
if label and (label in self.__list_fiducial_labels()):
if label and (label in gui_utils.list_fiducial_labels()):
for id_n in range(self.marker_list_ctrl.GetItemCount()):
item = self.marker_list_ctrl.GetItem(id_n, const.LABEL_COLUMN)
if item.GetText() == label:
Expand Down Expand Up @@ -3395,146 +3397,8 @@ def OnCreateMarker(
)
self.markers.AddMarker(marker, render=True, focus=True)

# Given a string, try to parse it as an integer, float, or string.
#
# TODO: This shouldn't be here, but there's currently no good place for the function.
# If csv-related functions are moved to a separate module, this function should be moved there.
def ParseValue(self, value):
value = value.strip()

# Handle None, booleans, empty list, and basic types
if value == "None":
return None
if value == "True":
return True
if value == "False":
return False
if value == "[]":
return []

# Handle lists and dictionaries
if value.startswith("[") and value.endswith("]"):
return self._parse_list(value)
if value.startswith("{") and value.endswith("}"):
return self._parse_dict(value)

# Try to convert to int or float
try:
if "." in value or "e" in value.lower():
return float(value)
return int(value)
except ValueError:
# Handle quoted strings
if (value.startswith('"') and value.endswith('"')) or (
value.startswith("'") and value.endswith("'")
):
return value[1:-1]
return value # Return as is if not recognized

def _parse_list(self, list_str):
"""Parse a list from string format."""
return [
self.ParseValue(el.strip())
for el in self._split_by_outer_commas(list_str[1:-1].strip())
]

def _parse_dict(self, dict_str):
"""Parse a dictionary from string format."""
items = self._split_by_outer_commas(dict_str[1:-1].strip())
return {
self.ParseValue(kv.split(":", 1)[0].strip()): self.ParseValue(
kv.split(":", 1)[1].strip()
)
for kv in items
}

def _split_by_outer_commas(self, string):
"""Split a string by commas that are not inside brackets or braces."""
elements = []
depth = 0
current_element = []

for char in string:
if char in "[{":
depth += 1
elif char in "]}" and depth > 0:
depth -= 1

if char == "," and depth == 0:
elements.append("".join(current_element).strip())
current_element = []
else:
current_element.append(char)

if current_element:
elements.append("".join(current_element).strip())

return elements

def GetMarkersFromFile(self, filename, overwrite_image_fiducials):
try:
with open(filename) as file:
magick_line = file.readline()
assert magick_line.startswith(const.MARKER_FILE_MAGICK_STRING)
version = int(magick_line.split("_")[-1])
if version not in const.SUPPORTED_MARKER_FILE_VERSIONS:
wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3"))
return

# Use the first line after the magick_line as the names for dictionary keys.
column_names = file.readline().strip().split("\t")
column_names_parsed = [self.ParseValue(name) for name in column_names]

markers_data = []
for line in file:
values = line.strip().split("\t")
values_parsed = [self.ParseValue(value) for value in values]
marker_data = dict(zip(column_names_parsed, values_parsed))

markers_data.append(marker_data)

self.marker_list_ctrl.Hide()

# Create markers from the dictionary.
for data in markers_data:
marker = Marker(version=version)
marker.from_dict(data)

# When loading markers from file, we first create a marker with is_target set to False, and then call __set_marker_as_target.
marker.is_target = False

# Note that we don't want to render or focus on the markers here for each loop iteration.
self.markers.AddMarker(marker, render=False)

if overwrite_image_fiducials and marker.label in self.__list_fiducial_labels():
Publisher.sendMessage(
"Load image fiducials", label=marker.label, position=marker.position
)

except Exception as e:
wx.MessageBox(_("Invalid markers file."), _("InVesalius 3"))
utils.debug(e)

self.marker_list_ctrl.Show()
Publisher.sendMessage("Render volume viewer")
Publisher.sendMessage("Update UI for refine tab")
self.markers.SaveState()

def OnLoadMarkers(self, evt):
"""Loads markers from file and appends them to the current marker list.
The file should contain no more than a single target marker. Also the
file should not contain any fiducials already in the list."""

last_directory = ses.Session().GetConfig("last_directory_3d_surface", "")
dialog = dlg.FileSelectionDialog(
_("Load markers"), last_directory, const.WILDCARD_MARKER_FILES
)
overwrite_checkbox = wx.CheckBox(dialog, -1, _("Overwrite current image fiducials"))
dialog.sizer.Add(overwrite_checkbox, 0, wx.CENTER)
dialog.FitSizers()
if dialog.ShowModal() == wx.ID_OK:
filename = dialog.GetPath()
self.GetMarkersFromFile(filename, overwrite_checkbox.GetValue())
dlg.ImportMarkers()

def OnShowHideAllMarkers(self, evt, ctrl):
if ctrl.GetValue():
Expand Down Expand Up @@ -3714,3 +3578,9 @@ def _AddMarker(self, marker, render, focus):
# Focus on the added marker.
if focus:
self.FocusOnMarker(num_items)

def _SetMarkersListRendering(self, render):
if render:
self.marker_list_ctrl.Show()
else:
self.marker_list_ctrl.Hide()
11 changes: 11 additions & 0 deletions invesalius/gui/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@
if TYPE_CHECKING:
import wx

import itertools

import invesalius.constants as const


def calc_width_needed(widget: "wx.Window", num_chars: int) -> int:
width, height = widget.GetTextExtent("M" * num_chars)
return width


def list_fiducial_labels():
"""Return the list of marker labels denoting fiducials."""
return list(
itertools.chain(*(const.BTNS_IMG_MARKERS[i].values() for i in const.BTNS_IMG_MARKERS))
)
Loading
Loading