From e38230f2ea7c1557987468ec3ef20ecc535976df Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Wed, 15 Jan 2025 17:31:29 +0200 Subject: [PATCH 01/16] ADD: New page in the coregistration panel for easier head surface creation with mask and threshold selection --- invesalius/constants.py | 9 ++ invesalius/gui/task_navigator.py | 148 ++++++++++++++++++++++++++++--- 2 files changed, 147 insertions(+), 10 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index fcb3f75fc..615e5aecb 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -820,6 +820,15 @@ BRAIN_INTENSITY_MTMS = 8 BRAIN_UUID = 9 +# Page order in the coregistration panel + +HEAD_PAGE = 0 +IMAGE_PAGE = 1 +TRACKER_PAGE = 2 +REFINE_PAGE = 3 +STYLUS_PAGE = 4 +STIMULATOR_PAGE = 5 + # ------------ Navigation defaults ------------------- MARKER_COLOUR = (1.0, 1.0, 0.0) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 40cf6bd6f..84ee9811c 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -45,6 +45,7 @@ import invesalius.constants as const import invesalius.data.coordinates as dco import invesalius.gui.dialogs as dlg +import invesalius.gui.widgets.gradient as grad import invesalius.project as prj import invesalius.session as ses from invesalius import inv_paths, utils @@ -318,6 +319,7 @@ def __init__(self, parent, nav_hub): self.tracker = nav_hub.tracker self.image = nav_hub.image + book.AddPage(HeadModelPage(book, nav_hub), _("Head")) book.AddPage(ImagePage(book, nav_hub), _("Image")) book.AddPage(TrackerPage(book, nav_hub), _("Patient")) book.AddPage(RefinePage(book, nav_hub), _("Refine")) @@ -335,6 +337,7 @@ def __init__(self, parent, nav_hub): self.__bind_events() def __bind_events(self): + Publisher.subscribe(self._FoldHead, "Move to head model page") Publisher.subscribe(self._FoldTracker, "Move to tracker page") Publisher.subscribe(self._FoldRefine, "Move to refine page") Publisher.subscribe(self._FoldStylus, "Move to stylus page") @@ -350,38 +353,154 @@ def OnPageChanged(self, evt): new_page = evt.GetSelection() # old page validations - if old_page == 0: + if old_page <= const.IMAGE_PAGE and new_page > const.IMAGE_PAGE: # Do not allow user to move to other (forward) tabs if image fiducials not done. if not self.image.AreImageFiducialsSet(): - self.book.SetSelection(0) + self.book.SetSelection(const.IMAGE_PAGE) wx.MessageBox(_("Please do the image registration first."), _("InVesalius 3")) - if old_page != 2: + if old_page != const.REFINE_PAGE: # Load data into refine tab Publisher.sendMessage("Update UI for refine tab") # new page validations - if (old_page == 1) and (new_page > 1): + if (old_page == const.TRACKER_PAGE) and (new_page > const.TRACKER_PAGE): # Do not allow user to move to other (forward) tabs if tracker fiducials not done. if self.image.AreImageFiducialsSet() and not self.tracker.AreTrackerFiducialsSet(): - self.book.SetSelection(1) + self.book.SetSelection(const.TRACKER_PAGE) wx.MessageBox(_("Please do the tracker registration first."), _("InVesalius 3")) # Unfold specific notebook pages + def _FoldHead(self): + self.book.SetSelection(const.HEAD_PAGE) + def _FoldImage(self): - self.book.SetSelection(0) + self.book.SetSelection(const.IMAGE_PAGE) def _FoldTracker(self): Publisher.sendMessage("Disable style", style=const.SLICE_STATE_CROSS) - self.book.SetSelection(1) + self.book.SetSelection(const.TRACKER_PAGE) def _FoldRefine(self): - self.book.SetSelection(2) + self.book.SetSelection(const.REFINE_PAGE) def _FoldStylus(self): - self.book.SetSelection(3) + self.book.SetSelection(const.STYLUS_PAGE) def _FoldStimulator(self): - self.book.SetSelection(4) + self.book.SetSelection(const.STIMULATOR_PAGE) + + +class HeadModelPage(wx.Panel): + def __init__(self, parent, nav_hub): + wx.Panel.__init__(self, parent) + + # Create sizers + top_sizer = wx.BoxSizer(wx.VERTICAL) + bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + main_sizer = wx.BoxSizer(wx.VERTICAL) + + # Add label above combo box + label_combo = wx.StaticText(self, label="Mask selection") + main_sizer.Add(label_combo, 0, wx.ALIGN_CENTER | wx.TOP, 10) + + # Create mask selection combo box + self.combo_box = wx.ComboBox(self, choices=[], style=wx.CB_READONLY) + top_sizer.Add(self.combo_box, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 20) + + # Add label above mask threshold bar + label_thresh = wx.StaticText(self, label="Threshold") + top_sizer.Add(label_thresh, 0, wx.ALIGN_CENTER | wx.TOP, 10) + + # Create mask threshold gradient bar + gradient = grad.GradientCtrl(self, -1, -5000, 5000, 0, 5000, (0, 255, 0, 100)) + self.gradient = gradient + top_sizer.Add(self.gradient, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) + + # Add next button + next_button = wx.Button(self, label="Next") + next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) + + bottom_sizer.AddStretchSpacer() + bottom_sizer.Add(next_button, 0, wx.ALIGN_CENTER) + bottom_sizer.AddStretchSpacer() + + # Main sizer config + main_sizer.Add(top_sizer, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) + main_sizer.AddStretchSpacer() + main_sizer.Add(bottom_sizer, 0, wx.EXPAND | wx.BOTTOM, 10) + + self.SetSizerAndFit(main_sizer) + self.Layout() + self.__bind_events() + self.__bind_events_wx() + + def OnNext(self, evt): + Publisher.sendMessage("Move to image page") + + def __bind_events(self): + Publisher.subscribe(self.SetThresholdBounds, "Update threshold limits") + Publisher.subscribe(self.SetThresholdValues, "Set threshold values in gradient") + Publisher.subscribe(self.SetThresholdValues2, "Set threshold values") + Publisher.subscribe(self.SelectMaskName, "Select mask name in combo") + Publisher.subscribe(self.SetItemsColour, "Set GUI items colour") + Publisher.subscribe(self.OnRemoveMasks, "Remove masks") + Publisher.subscribe(self.AddMask, "Add mask") + + def __bind_events_wx(self): + self.combo_box.Bind(wx.EVT_COMBOBOX, self.OnComboName) + self.Bind(grad.EVT_THRESHOLD_CHANGED, self.OnSlideChanged, self.gradient) + self.Bind(grad.EVT_THRESHOLD_CHANGING, self.OnSlideChanging, self.gradient) + + def OnComboName(self, evt): + mask_index = evt.GetSelection() + Publisher.sendMessage("Change mask selected", index=mask_index) + Publisher.sendMessage("Show mask", index=mask_index, value=True) + + def AddMask(self, mask): + self.combo_box.Append(mask.name) + + def SelectMaskName(self, index): + if index >= 0: + self.combo_box.SetSelection(index) + else: + self.combo_box.SetValue("") + + def OnRemoveMasks(self, mask_indexes): + for i in mask_indexes: + self.combo_box.Delete(i) + + def SetThresholdBounds(self, threshold_range): + thresh_min = threshold_range[0] + thresh_max = threshold_range[1] + self.gradient.SetMinRange(thresh_min) + self.gradient.SetMaxRange(thresh_max) + + def SetThresholdValues(self, threshold_range): + thresh_min, thresh_max = threshold_range + self.gradient.SetMinValue(thresh_min) + self.gradient.SetMaxValue(thresh_max) + + def SetThresholdValues2(self, threshold_range): + thresh_min, thresh_max = threshold_range + self.gradient.SetMinValue(thresh_min) + self.gradient.SetMaxValue(thresh_max) + + def OnSlideChanged(self, evt): + thresh_min = self.gradient.GetMinValue() + thresh_max = self.gradient.GetMaxValue() + Publisher.sendMessage("Set threshold values", threshold_range=(thresh_min, thresh_max)) + session = ses.Session() + session.ChangeProject() + + def OnSlideChanging(self, evt): + thresh_min = self.gradient.GetMinValue() + thresh_max = self.gradient.GetMaxValue() + Publisher.sendMessage("Changing threshold values", threshold_range=(thresh_min, thresh_max)) + session = ses.Session() + session.ChangeProject() + + def SetItemsColour(self, colour): + self.gradient.SetColour(colour) class ImagePage(wx.Panel): @@ -428,10 +547,16 @@ def __init__(self, parent, nav_hub): next_button.Disable() self.next_button = next_button + back_button = wx.Button(self, label="Back") + back_button.Bind(wx.EVT_BUTTON, partial(self.OnBack)) + self.back_button = back_button + top_sizer = wx.BoxSizer(wx.HORIZONTAL) top_sizer.AddMany([(start_button), (reset_button)]) bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + bottom_sizer.Add(back_button) + bottom_sizer.AddSpacer(120) bottom_sizer.Add(next_button) sizer = wx.GridBagSizer(5, 5) @@ -557,6 +682,9 @@ def OnReset(self, evt, ctrl): self.image.ResetImageFiducials() self.OnResetImageFiducials() + def OnBack(self, evt): + Publisher.sendMessage("Move to head model page") + def OnResetImageFiducials(self): self.next_button.Disable() for ctrl in self.btns_set_fiducial: From 2ba15cac57ce3aa20e443bff285d0ccba4ab7ba5 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Wed, 15 Jan 2025 18:01:43 +0200 Subject: [PATCH 02/16] ADD: Button in coregistration to create head surface from mask and then a new surface from the largest contigous region --- invesalius/data/surface.py | 3 ++ invesalius/gui/dialogs.py | 19 +++++++++ invesalius/gui/task_navigator.py | 67 ++++++++++++++++++++++++++++---- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index c2822e0e9..b1fe586f2 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -315,6 +315,8 @@ def OnLargestSurface(self): Create a new surface, based on largest part of the last selected surface. """ + progress_dialog = dialogs.SelectLargestSurfaceProgressWindow() + progress_dialog.Update() index = self.last_surface_index proj = prj.Project() surface = proj.surface_dict[index] @@ -322,6 +324,7 @@ def OnLargestSurface(self): new_polydata = pu.SelectLargestPart(surface.polydata) new_index = self.CreateSurfaceFromPolydata(new_polydata) Publisher.sendMessage("Show single surface", index=new_index, visibility=True) + progress_dialog.Close() def OnImportCustomBinFile(self, filename): import os diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 7333d700c..1e4d52cf0 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1978,6 +1978,25 @@ def GetValue(self) -> Dict[str, Union[str, int, bool]]: } +class SelectLargestSurfaceProgressWindow: + def __init__(self): + title = "InVesalius 3" + message = "Creating a new surface form the largest contiguous region..." + style = wx.PD_APP_MODAL | wx.PD_CAN_ABORT + parent = wx.GetApp().GetTopWindow() + self.dlg = wx.ProgressDialog(title, message, parent=parent, style=style) + self.dlg.Show() + + def Update(self, msg: Optional[str] = None, value=None) -> None: + if msg is None: + self.dlg.Pulse() + else: + self.dlg.Pulse(msg) + + def Close(self) -> None: + self.dlg.Destroy() + + class SurfaceTransparencyDialog(wx.Dialog): def __init__( self, parent: Optional[wx.Window], surface_index: int = 0, transparency: int = 0 diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 84ee9811c..102dcfff1 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -44,6 +44,7 @@ import invesalius.constants as const import invesalius.data.coordinates as dco +import invesalius.data.slice_ as slice_ import invesalius.gui.dialogs as dlg import invesalius.gui.widgets.gradient as grad import invesalius.project as prj @@ -404,8 +405,8 @@ def __init__(self, parent, nav_hub): main_sizer.Add(label_combo, 0, wx.ALIGN_CENTER | wx.TOP, 10) # Create mask selection combo box - self.combo_box = wx.ComboBox(self, choices=[], style=wx.CB_READONLY) - top_sizer.Add(self.combo_box, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 20) + self.combo_mask = wx.ComboBox(self, choices=[], style=wx.CB_READONLY) + top_sizer.Add(self.combo_mask, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 20) # Add label above mask threshold bar label_thresh = wx.StaticText(self, label="Threshold") @@ -416,6 +417,12 @@ def __init__(self, parent, nav_hub): self.gradient = gradient top_sizer.Add(self.gradient, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) + # Add create surface button + create_head_button = wx.Button(self, label="Create 3D head") + create_head_button.Bind(wx.EVT_BUTTON, partial(self.OnCreateHead)) + bottom_sizer.AddStretchSpacer() + bottom_sizer.Add(create_head_button, 0, wx.ALIGN_CENTER) + # Add next button next_button = wx.Button(self, label="Next") next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) @@ -447,7 +454,7 @@ def __bind_events(self): Publisher.subscribe(self.AddMask, "Add mask") def __bind_events_wx(self): - self.combo_box.Bind(wx.EVT_COMBOBOX, self.OnComboName) + self.combo_mask.Bind(wx.EVT_COMBOBOX, self.OnComboName) self.Bind(grad.EVT_THRESHOLD_CHANGED, self.OnSlideChanged, self.gradient) self.Bind(grad.EVT_THRESHOLD_CHANGING, self.OnSlideChanging, self.gradient) @@ -457,17 +464,17 @@ def OnComboName(self, evt): Publisher.sendMessage("Show mask", index=mask_index, value=True) def AddMask(self, mask): - self.combo_box.Append(mask.name) + self.combo_mask.Append(mask.name) def SelectMaskName(self, index): if index >= 0: - self.combo_box.SetSelection(index) + self.combo_mask.SetSelection(index) else: - self.combo_box.SetValue("") + self.combo_mask.SetValue("") def OnRemoveMasks(self, mask_indexes): for i in mask_indexes: - self.combo_box.Delete(i) + self.combo_mask.Delete(i) def SetThresholdBounds(self, threshold_range): thresh_min = threshold_range[0] @@ -502,6 +509,52 @@ def OnSlideChanging(self, evt): def SetItemsColour(self, colour): self.gradient.SetColour(colour) + def OnCreateHead(self, evt): + self.OnCreateSurface(evt) + self.SelectLargestSurface() + + def OnCreateSurface(self, evt): + algorithm = "Default" + options = {} + to_generate = True + if self.combo_mask.GetSelection() != -1: + sl = slice_.Slice() + if sl.current_mask.was_edited: + dlgs = dlg.SurfaceDialog() + if dlgs.ShowModal() == wx.ID_OK: + algorithm = dlgs.GetAlgorithmSelected() + options = dlgs.GetOptions() + else: + to_generate = False + dlgs.Destroy() + if to_generate: + proj = prj.Project() + for idx in proj.mask_dict: + if proj.mask_dict[idx] is sl.current_mask: + mask_index = idx + break + else: + return + method = {"algorithm": algorithm, "options": options} + srf_options = { + "index": mask_index, + "name": "", + "quality": _("Optimal *"), + "fill": False, + "keep_largest": False, + "overwrite": False, + } + Publisher.sendMessage( + "Create surface from index", + surface_parameters={"method": method, "options": srf_options}, + ) + Publisher.sendMessage("Fold surface task") + else: + dlg.InexistentMask() + + def SelectLargestSurface(self): + Publisher.sendMessage("Create surface from largest region") + class ImagePage(wx.Panel): def __init__(self, parent, nav_hub): From a1b18270acd4512bf30929b47543b39acd9cfd71 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Thu, 16 Jan 2025 17:03:45 +0200 Subject: [PATCH 03/16] ADD: Streamlined head surface creation also removes non-visible faces if the plugin is installed --- invesalius/gui/task_navigator.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 102dcfff1..d211f86d3 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -32,6 +32,7 @@ except Exception: has_mTMS = False +import importlib.util import sys import uuid @@ -393,6 +394,7 @@ def _FoldStimulator(self): class HeadModelPage(wx.Panel): def __init__(self, parent, nav_hub): + self.remove_non_visible_faces = False wx.Panel.__init__(self, parent) # Create sizers @@ -418,7 +420,7 @@ def __init__(self, parent, nav_hub): top_sizer.Add(self.gradient, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) # Add create surface button - create_head_button = wx.Button(self, label="Create 3D head") + create_head_button = wx.Button(self, label="Create head surface") create_head_button.Bind(wx.EVT_BUTTON, partial(self.OnCreateHead)) bottom_sizer.AddStretchSpacer() bottom_sizer.Add(create_head_button, 0, wx.ALIGN_CENTER) @@ -450,6 +452,7 @@ def __bind_events(self): Publisher.subscribe(self.SetThresholdValues2, "Set threshold values") Publisher.subscribe(self.SelectMaskName, "Select mask name in combo") Publisher.subscribe(self.SetItemsColour, "Set GUI items colour") + Publisher.subscribe(self.OnPluginsFound, "Add plugins menu items") Publisher.subscribe(self.OnRemoveMasks, "Remove masks") Publisher.subscribe(self.AddMask, "Add mask") @@ -506,12 +509,19 @@ def OnSlideChanging(self, evt): session = ses.Session() session.ChangeProject() + def OnPluginsFound(self, items): + print("OnPluginsFound in task_navigator.py") + for item in items: + if item == "Remove non-visible faces": + self.remove_non_visible_faces = True + def SetItemsColour(self, colour): self.gradient.SetColour(colour) def OnCreateHead(self, evt): self.OnCreateSurface(evt) self.SelectLargestSurface() + self.RemoveNonVisibleFaces() def OnCreateSurface(self, evt): algorithm = "Default" @@ -555,6 +565,19 @@ def OnCreateSurface(self, evt): def SelectLargestSurface(self): Publisher.sendMessage("Create surface from largest region") + def RemoveNonVisibleFaces(self): + plugin_name = "Remove non-visible faces" + + # Check that the remove non-visible faces plugin is installed + if self.remove_non_visible_faces: + # Try to import and run the plugin + try: + main = importlib.import_module(plugin_name + ".main") + main.load() + # Make sure the plugin has been loaded + except ModuleNotFoundError: + Publisher.sendMessage("Load plugin", plugin_name=plugin_name) + class ImagePage(wx.Panel): def __init__(self, parent, nav_hub): From 2c227a6ee938826f8e5890852e939e0e91f01e96 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Fri, 17 Jan 2025 15:48:11 +0200 Subject: [PATCH 04/16] ADD: Bypass plugin GUI when removing non-visible faces in streamlined head surface creation --- invesalius/gui/dialogs.py | 19 ++++++++++++++ invesalius/gui/task_navigator.py | 20 +++++---------- invesalius/plugins.py | 44 +++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 1e4d52cf0..6b2786f15 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1997,6 +1997,25 @@ def Close(self) -> None: self.dlg.Destroy() +class RemoveNonVisibleFacesProgressWindow: + def __init__(self): + title = "InVesalius 3" + message = "Removing non-visible faces..." + style = wx.PD_APP_MODAL | wx.PD_CAN_ABORT + parent = wx.GetApp().GetTopWindow() + self.dlg = wx.ProgressDialog(title, message, parent=parent, style=style) + self.dlg.Show() + + def Update(self, msg: Optional[str] = None, value=None) -> None: + if msg is None: + self.dlg.Pulse() + else: + self.dlg.Pulse(msg) + + def Close(self) -> None: + self.dlg.Destroy() + + class SurfaceTransparencyDialog(wx.Dialog): def __init__( self, parent: Optional[wx.Window], surface_index: int = 0, transparency: int = 0 diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index d211f86d3..d5256b371 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -32,7 +32,6 @@ except Exception: has_mTMS = False -import importlib.util import sys import uuid @@ -421,7 +420,7 @@ def __init__(self, parent, nav_hub): # Add create surface button create_head_button = wx.Button(self, label="Create head surface") - create_head_button.Bind(wx.EVT_BUTTON, partial(self.OnCreateHead)) + create_head_button.Bind(wx.EVT_BUTTON, partial(self.OnCreateHeadSurface)) bottom_sizer.AddStretchSpacer() bottom_sizer.Add(create_head_button, 0, wx.ALIGN_CENTER) @@ -510,15 +509,17 @@ def OnSlideChanging(self, evt): session.ChangeProject() def OnPluginsFound(self, items): - print("OnPluginsFound in task_navigator.py") for item in items: if item == "Remove non-visible faces": self.remove_non_visible_faces = True + break def SetItemsColour(self, colour): self.gradient.SetColour(colour) - def OnCreateHead(self, evt): + # Creates the head surface from a mask, selects the largest contiguous region, + # and removes non-visible faces if the plugin is installed + def OnCreateHeadSurface(self, evt): self.OnCreateSurface(evt) self.SelectLargestSurface() self.RemoveNonVisibleFaces() @@ -566,17 +567,8 @@ def SelectLargestSurface(self): Publisher.sendMessage("Create surface from largest region") def RemoveNonVisibleFaces(self): - plugin_name = "Remove non-visible faces" - - # Check that the remove non-visible faces plugin is installed if self.remove_non_visible_faces: - # Try to import and run the plugin - try: - main = importlib.import_module(plugin_name + ".main") - main.load() - # Make sure the plugin has been loaded - except ModuleNotFoundError: - Publisher.sendMessage("Load plugin", plugin_name=plugin_name) + Publisher.sendMessage("Remove non-visible faces") class ImagePage(wx.Panel): diff --git a/invesalius/plugins.py b/invesalius/plugins.py index 05af7ba4c..0378c6fc1 100644 --- a/invesalius/plugins.py +++ b/invesalius/plugins.py @@ -26,8 +26,10 @@ from types import ModuleType from typing import TYPE_CHECKING -from invesalius import inv_paths +from invesalius import inv_paths, project +from invesalius.gui import dialogs from invesalius.pubsub import pub as Publisher +from invesalius.utils import new_name_by_pattern if TYPE_CHECKING: import os @@ -51,6 +53,7 @@ def __init__(self): def __bind_pubsub_evt(self) -> None: Publisher.subscribe(self.load_plugin, "Load plugin") + Publisher.subscribe(self.remove_non_visible_faces_no_gui, "Remove non-visible faces") def find_plugins(self) -> None: self.plugins = {} @@ -87,3 +90,42 @@ def load_plugin(self, plugin_name: str) -> None: sys.modules[plugin_name] = plugin_module main = importlib.import_module(plugin_name + ".main") main.load() + + # Remove non-visible faces from a surface without using the plugin GUI. + # Defaults to the last surface in surface_dict which is generally the newest surface + def remove_non_visible_faces_no_gui(self, surface_idx: int = -1) -> None: + plugin_name = "Remove non-visible faces" + if plugin_name in self.plugins: + progress_dialog = dialogs.RemoveNonVisibleFacesProgressWindow() + progress_dialog.Update() + plugin_module = import_source( + plugin_name, self.plugins[plugin_name]["folder"].joinpath("__init__.py") + ) + sys.modules[plugin_name] = plugin_module + remove_faces = importlib.import_module(plugin_name + ".remove_non_visible_faces") + + inv_proj = project.Project() + try: + surface = list(inv_proj.surface_dict.values())[surface_idx] + except IndexError: + print(f"Invalid surface_dict index {surface_idx}, did not remove non-visible faces") + return + + overwrite = False + new_polydata = remove_faces.remove_non_visible_faces( + surface.polydata, remove_visible=False + ) + + name = new_name_by_pattern(f"{surface.name}_removed_nonvisible") + colour = None + + Publisher.sendMessage( + "Create surface from polydata", + polydata=new_polydata, + name=name, + overwrite=overwrite, + index=surface_idx, + colour=colour, + ) + Publisher.sendMessage("Fold surface task") + progress_dialog.Close() From 1684397926e9bbd45a66f0f7b51f110886291fd8 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Fri, 17 Jan 2025 16:30:55 +0200 Subject: [PATCH 05/16] MOD: Rename 'Head' page in coregistration panel as 'Surface' to be more descriptive --- invesalius/constants.py | 2 +- invesalius/gui/task_navigator.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index 615e5aecb..9075c5938 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -822,7 +822,7 @@ # Page order in the coregistration panel -HEAD_PAGE = 0 +SURFACE_PAGE = 0 IMAGE_PAGE = 1 TRACKER_PAGE = 2 REFINE_PAGE = 3 diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index d5256b371..36ae463bf 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -320,7 +320,7 @@ def __init__(self, parent, nav_hub): self.tracker = nav_hub.tracker self.image = nav_hub.image - book.AddPage(HeadModelPage(book, nav_hub), _("Head")) + book.AddPage(SurfacePage(book, nav_hub), _("Surface")) book.AddPage(ImagePage(book, nav_hub), _("Image")) book.AddPage(TrackerPage(book, nav_hub), _("Patient")) book.AddPage(RefinePage(book, nav_hub), _("Refine")) @@ -372,7 +372,7 @@ def OnPageChanged(self, evt): # Unfold specific notebook pages def _FoldHead(self): - self.book.SetSelection(const.HEAD_PAGE) + self.book.SetSelection(const.SURFACE_PAGE) def _FoldImage(self): self.book.SetSelection(const.IMAGE_PAGE) @@ -391,7 +391,7 @@ def _FoldStimulator(self): self.book.SetSelection(const.STIMULATOR_PAGE) -class HeadModelPage(wx.Panel): +class SurfacePage(wx.Panel): def __init__(self, parent, nav_hub): self.remove_non_visible_faces = False wx.Panel.__init__(self, parent) From 160981efba0dbc45b9acb64f33e73e763b05432f Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Wed, 29 Jan 2025 17:16:36 +0200 Subject: [PATCH 06/16] ADD: checkboxes to control the steps of the streamlined head surface creation --- invesalius/gui/task_navigator.py | 34 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 36ae463bf..dc108ce18 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -393,12 +393,11 @@ def _FoldStimulator(self): class SurfacePage(wx.Panel): def __init__(self, parent, nav_hub): - self.remove_non_visible_faces = False wx.Panel.__init__(self, parent) # Create sizers top_sizer = wx.BoxSizer(wx.VERTICAL) - bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + bottom_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer = wx.BoxSizer(wx.VERTICAL) # Add label above combo box @@ -418,19 +417,30 @@ def __init__(self, parent, nav_hub): self.gradient = gradient top_sizer.Add(self.gradient, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) + # Checkbox for selecting the largest surface + self.select_largest_surface_checkbox = wx.CheckBox(self, label="Select largest surface") + top_sizer.AddStretchSpacer(2) + top_sizer.Add(self.select_largest_surface_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) + top_sizer.AddSpacer(5) + self.select_largest_surface_checkbox.SetValue(True) + + # Checkbox for removing non-visible faces + self.remove_non_visible_checkbox = wx.CheckBox(self, label="Remove non-visible faces") + top_sizer.Add(self.remove_non_visible_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) + + # Disable the checkbox by default and enable it only if the plugin is found + self.remove_non_visible_checkbox.Disable() + # Add create surface button create_head_button = wx.Button(self, label="Create head surface") create_head_button.Bind(wx.EVT_BUTTON, partial(self.OnCreateHeadSurface)) - bottom_sizer.AddStretchSpacer() - bottom_sizer.Add(create_head_button, 0, wx.ALIGN_CENTER) + top_sizer.AddStretchSpacer() + top_sizer.Add(create_head_button, 0, wx.ALIGN_CENTER) # Add next button next_button = wx.Button(self, label="Next") next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) - - bottom_sizer.AddStretchSpacer() - bottom_sizer.Add(next_button, 0, wx.ALIGN_CENTER) - bottom_sizer.AddStretchSpacer() + bottom_sizer.Add(next_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10) # Main sizer config main_sizer.Add(top_sizer, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) @@ -511,7 +521,8 @@ def OnSlideChanging(self, evt): def OnPluginsFound(self, items): for item in items: if item == "Remove non-visible faces": - self.remove_non_visible_faces = True + self.remove_non_visible_checkbox.Enable() + self.remove_non_visible_checkbox.SetValue(True) break def SetItemsColour(self, colour): @@ -564,10 +575,11 @@ def OnCreateSurface(self, evt): dlg.InexistentMask() def SelectLargestSurface(self): - Publisher.sendMessage("Create surface from largest region") + if self.select_largest_surface_checkbox.IsChecked(): + Publisher.sendMessage("Create surface from largest region") def RemoveNonVisibleFaces(self): - if self.remove_non_visible_faces: + if self.remove_non_visible_checkbox.IsChecked(): Publisher.sendMessage("Remove non-visible faces") From e35437025fde62a606a833d5df7605473f16f0d0 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Thu, 6 Feb 2025 15:44:29 +0200 Subject: [PATCH 07/16] ADD: Brain segmentation step to the streamlined head surface creation --- invesalius/constants.py | 2 +- invesalius/gui/deep_learning_seg_dialog.py | 22 ++++- invesalius/gui/task_navigator.py | 109 +++++++++++++++++---- 3 files changed, 110 insertions(+), 23 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index 9075c5938..615e5aecb 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -822,7 +822,7 @@ # Page order in the coregistration panel -SURFACE_PAGE = 0 +HEAD_PAGE = 0 IMAGE_PAGE = 1 TRACKER_PAGE = 2 REFINE_PAGE = 3 diff --git a/invesalius/gui/deep_learning_seg_dialog.py b/invesalius/gui/deep_learning_seg_dialog.py index 4527aa3e6..b4d77ca32 100644 --- a/invesalius/gui/deep_learning_seg_dialog.py +++ b/invesalius/gui/deep_learning_seg_dialog.py @@ -48,7 +48,14 @@ class DeepLearningSegmenterDialog(wx.Dialog): def __init__( - self, parent, title, has_torch=True, has_plaidml=True, has_theano=True, segmenter=None + self, + parent, + title, + close_on_completion=False, + has_torch=True, + has_plaidml=True, + has_theano=True, + segmenter=None, ): wx.Dialog.__init__( self, @@ -68,6 +75,7 @@ def __init__( # self.pg_dialog = None self.torch_devices = TORCH_DEVICES self.plaidml_devices = PLAIDML_DEVICES + self.close_on_completion = close_on_completion self.backends = backends @@ -332,6 +340,10 @@ def AfterSegment(self): self.elapsed_time_timer.Stop() self.apply_segment_threshold() + if self.close_on_completion: + self.OnClose(self) + Publisher.sendMessage("Brain segmentation completed") + def SetProgress(self, progress): self.progress.SetValue(int(progress * 100)) wx.GetApp().Yield() @@ -361,8 +373,9 @@ def OnTickTimer(self, evt): if progress == np.inf: progress = 1 self.AfterSegment() - progress = max(0, min(progress, 1)) - self.SetProgress(float(progress)) + else: + progress = max(0, min(progress, 1)) + self.SetProgress(float(progress)) def OnClose(self, evt): # self.segmenter.stop = True @@ -393,7 +406,7 @@ def ShowProgress(self): class BrainSegmenterDialog(DeepLearningSegmenterDialog): - def __init__(self, parent): + def __init__(self, parent, close_on_completion=False): super().__init__( parent=parent, title=_("Brain segmentation"), @@ -401,6 +414,7 @@ def __init__(self, parent): has_plaidml=True, has_theano=True, segmenter=segment.BrainSegmentProcess, + close_on_completion=close_on_completion, ) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index dc108ce18..3b7724d79 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -51,6 +51,7 @@ import invesalius.session as ses from invesalius import inv_paths, utils from invesalius.data.markers.marker import Marker, MarkerType +from invesalius.gui import deep_learning_seg_dialog from invesalius.gui.widgets.fiducial_buttons import OrderedFiducialButtons from invesalius.i18n import tr as _ from invesalius.navigation.navigation import NavigationHub @@ -320,7 +321,7 @@ def __init__(self, parent, nav_hub): self.tracker = nav_hub.tracker self.image = nav_hub.image - book.AddPage(SurfacePage(book, nav_hub), _("Surface")) + book.AddPage(HeadPage(book, nav_hub), _("Head")) book.AddPage(ImagePage(book, nav_hub), _("Image")) book.AddPage(TrackerPage(book, nav_hub), _("Patient")) book.AddPage(RefinePage(book, nav_hub), _("Refine")) @@ -372,7 +373,7 @@ def OnPageChanged(self, evt): # Unfold specific notebook pages def _FoldHead(self): - self.book.SetSelection(const.SURFACE_PAGE) + self.book.SetSelection(const.HEAD_PAGE) def _FoldImage(self): self.book.SetSelection(const.IMAGE_PAGE) @@ -391,7 +392,7 @@ def _FoldStimulator(self): self.book.SetSelection(const.STIMULATOR_PAGE) -class SurfacePage(wx.Panel): +class HeadPage(wx.Panel): def __init__(self, parent, nav_hub): wx.Panel.__init__(self, parent) @@ -427,10 +428,15 @@ def __init__(self, parent, nav_hub): # Checkbox for removing non-visible faces self.remove_non_visible_checkbox = wx.CheckBox(self, label="Remove non-visible faces") top_sizer.Add(self.remove_non_visible_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) + top_sizer.AddSpacer(5) # Disable the checkbox by default and enable it only if the plugin is found self.remove_non_visible_checkbox.Disable() + # Checkbox for brain segmentation + self.brain_segmentation_checkbox = wx.CheckBox(self, label="Brain segmentation") + top_sizer.Add(self.brain_segmentation_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) + # Add create surface button create_head_button = wx.Button(self, label="Create head surface") create_head_button.Bind(wx.EVT_BUTTON, partial(self.OnCreateHeadSurface)) @@ -456,6 +462,7 @@ def OnNext(self, evt): Publisher.sendMessage("Move to image page") def __bind_events(self): + Publisher.subscribe(self.OnSuccessfulBrainSegmentation, "Brain segmentation completed") Publisher.subscribe(self.SetThresholdBounds, "Update threshold limits") Publisher.subscribe(self.SetThresholdValues, "Set threshold values in gradient") Publisher.subscribe(self.SetThresholdValues2, "Set threshold values") @@ -528,27 +535,65 @@ def OnPluginsFound(self, items): def SetItemsColour(self, colour): self.gradient.SetColour(colour) - # Creates the head surface from a mask, selects the largest contiguous region, - # and removes non-visible faces if the plugin is installed + # Creates the head surface from a mask, and depending on the checkboxes + # selects the largest surface, removes non-visible faces, and does brain segmentation def OnCreateHeadSurface(self, evt): - self.OnCreateSurface(evt) - self.SelectLargestSurface() - self.RemoveNonVisibleFaces() + self.CreateSurface(evt) + + if self.select_largest_surface_checkbox.IsChecked(): + self.SelectLargestSurface() + + if self.remove_non_visible_checkbox.IsChecked(): + self.RemoveNonVisibleFaces() + + if self.brain_segmentation_checkbox.IsChecked(): + self.SegmentBrain() + + def CreateBrainSurface(self): + options = {"angle": 0.7, "max distance": 3.0, "min weight": 0.5, "steps": 10} + algorithm = "ca_smoothing" + proj = prj.Project() + mask_index = len(proj.mask_dict) - 1 + + if self.combo_mask.GetSelection() != -1: + sl = slice_.Slice() + for idx in proj.mask_dict: + if proj.mask_dict[idx] is sl.current_mask: + mask_index = idx + break + + method = {"algorithm": algorithm, "options": options} + srf_options = { + "index": mask_index, + "name": "", + "quality": _("Optimal *"), + "fill": False, + "keep_largest": False, + "overwrite": False, + } + Publisher.sendMessage( + "Create surface from index", + surface_parameters={"method": method, "options": srf_options}, + ) + Publisher.sendMessage("Fold surface task") + + else: + dlg.InexistentMask() - def OnCreateSurface(self, evt): + def CreateSurface(self, evt): algorithm = "Default" options = {} to_generate = True if self.combo_mask.GetSelection() != -1: sl = slice_.Slice() if sl.current_mask.was_edited: - dlgs = dlg.SurfaceDialog() - if dlgs.ShowModal() == wx.ID_OK: - algorithm = dlgs.GetAlgorithmSelected() - options = dlgs.GetOptions() + surface_dlg = dlg.SurfaceDialog() + if surface_dlg.ShowModal() == wx.ID_OK: + algorithm = surface_dlg.GetAlgorithmSelected() + options = surface_dlg.GetOptions() else: to_generate = False - dlgs.Destroy() + surface_dlg.Destroy() if to_generate: proj = prj.Project() for idx in proj.mask_dict: @@ -575,12 +620,40 @@ def OnCreateSurface(self, evt): dlg.InexistentMask() def SelectLargestSurface(self): - if self.select_largest_surface_checkbox.IsChecked(): - Publisher.sendMessage("Create surface from largest region") + Publisher.sendMessage("Create surface from largest region") def RemoveNonVisibleFaces(self): - if self.remove_non_visible_checkbox.IsChecked(): - Publisher.sendMessage("Remove non-visible faces") + Publisher.sendMessage("Remove non-visible faces") + + def OnSuccessfulBrainSegmentation(self): + self.CreateBrainSurface() + proj = prj.Project() + idx = len(proj.surface_dict) - 1 + Publisher.sendMessage("Show single surface", index=idx, visibility=True) + + def SegmentBrain(self): + if ( + deep_learning_seg_dialog.HAS_PLAIDML + or deep_learning_seg_dialog.HAS_THEANO + or deep_learning_seg_dialog.HAS_TORCH + ): + segmentation_dlg = deep_learning_seg_dialog.BrainSegmenterDialog( + self, close_on_completion=True + ) + segmentation_dlg.CenterOnScreen() + segmentation_dlg.Show() + else: + segmentation_dlg = wx.MessageDialog( + self, + _( + "It's not possible to run brain segmenter because your system doesn't have the following modules installed:" + ) + + " Torch, PlaidML or Theano", + "InVesalius 3 - Brain segmenter", + wx.ICON_INFORMATION | wx.OK, + ) + segmentation_dlg.ShowModal() + segmentation_dlg.Destroy() class ImagePage(wx.Panel): From 8d11ed99428f93deacffdd53f011022948b81653 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Fri, 14 Feb 2025 14:12:46 +0200 Subject: [PATCH 08/16] MOD: Show the navigation panel by default when opening InVesalius in navigation mode --- invesalius/gui/default_tasks.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index 958d15ad9..da887f13f 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -331,7 +331,6 @@ def __init__(self, parent): elif name == _("Configure 3D surface"): self.__id_surface = item.GetId() - fold_panel.Expand(fold_panel.GetFoldPanel(0)) self.fold_panel = fold_panel self.image_list = image_list @@ -342,6 +341,12 @@ def __init__(self, parent): self.SetSizerAndFit(sizer) self.__bind_events() + # Show the navigation panel in navigation mode; otherwise, show the imports panel + if mode == const.MODE_NAVIGATOR: + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(1)) + else: + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) + def __bind_events(self): session = ses.Session() mode = session.GetConfig("mode") From 6799dd85b8102cfe9940b29a2ca113d44a74807e Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Fri, 14 Feb 2025 15:48:48 +0200 Subject: [PATCH 09/16] MOD: Set default colors and transparency for the created scalp and brain surfaces --- invesalius/gui/task_navigator.py | 37 +++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 3b7724d79..ed9063e1e 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -554,6 +554,7 @@ def CreateBrainSurface(self): algorithm = "ca_smoothing" proj = prj.Project() mask_index = len(proj.mask_dict) - 1 + brain_colour = [235, 245, 255] if self.combo_mask.GetSelection() != -1: sl = slice_.Slice() @@ -577,6 +578,20 @@ def CreateBrainSurface(self): ) Publisher.sendMessage("Fold surface task") + surface_idx = len(proj.surface_dict) - 1 + brain_vtk_colour = [c / 255.0 for c in brain_colour] + + Publisher.sendMessage( + "Set surface colour", surface_index=surface_idx, colour=brain_vtk_colour + ) + + # Select the edited surface to update the color in the surface properties GUI + Publisher.sendMessage("Change surface selected", surface_index=surface_idx) + + # Visualize the scalp and brain surfaces + last_two = list(range(len(proj.surface_dict) - 2, len(proj.surface_dict))) + Publisher.sendMessage("Show multiple surfaces", index_list=last_two, visibility=True) + else: dlg.InexistentMask() @@ -625,11 +640,27 @@ def SelectLargestSurface(self): def RemoveNonVisibleFaces(self): Publisher.sendMessage("Remove non-visible faces") + proj = prj.Project() + surface_idx = len(proj.surface_dict) - 1 + scalp_colour = [255, 235, 255] + transparency = 0.25 + scalp_vtk_colour = [c / 255.0 for c in scalp_colour] + + Publisher.sendMessage( + "Set surface colour", surface_index=surface_idx, colour=scalp_vtk_colour + ) + Publisher.sendMessage( + "Set surface transparency", surface_index=surface_idx, transparency=transparency + ) + + # Select the edited surface to update the color in the surface properties GUI + Publisher.sendMessage("Change surface selected", surface_index=surface_idx) + + # Hide other surfaces + Publisher.sendMessage("Show single surface", index=surface_idx, visibility=True) + def OnSuccessfulBrainSegmentation(self): self.CreateBrainSurface() - proj = prj.Project() - idx = len(proj.surface_dict) - 1 - Publisher.sendMessage("Show single surface", index=idx, visibility=True) def SegmentBrain(self): if ( From d69b7a79c47dea3ff1b288eeec9d77a09c37b49b Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Fri, 14 Feb 2025 16:05:42 +0200 Subject: [PATCH 10/16] MOD: Automate the brain segmentation process with default parameters --- invesalius/gui/deep_learning_seg_dialog.py | 13 ++++++++----- invesalius/gui/task_navigator.py | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/invesalius/gui/deep_learning_seg_dialog.py b/invesalius/gui/deep_learning_seg_dialog.py index b4d77ca32..82b241235 100644 --- a/invesalius/gui/deep_learning_seg_dialog.py +++ b/invesalius/gui/deep_learning_seg_dialog.py @@ -51,7 +51,7 @@ def __init__( self, parent, title, - close_on_completion=False, + auto_segment=False, has_torch=True, has_plaidml=True, has_theano=True, @@ -75,7 +75,7 @@ def __init__( # self.pg_dialog = None self.torch_devices = TORCH_DEVICES self.plaidml_devices = PLAIDML_DEVICES - self.close_on_completion = close_on_completion + self.auto_segment = auto_segment self.backends = backends @@ -95,6 +95,9 @@ def __init__( self.OnSetBackend() self.HideProgress() + if self.auto_segment: + self.OnSegment(self) + def _init_gui(self): self.cb_backends = wx.ComboBox( self, @@ -340,7 +343,7 @@ def AfterSegment(self): self.elapsed_time_timer.Stop() self.apply_segment_threshold() - if self.close_on_completion: + if self.auto_segment: self.OnClose(self) Publisher.sendMessage("Brain segmentation completed") @@ -406,7 +409,7 @@ def ShowProgress(self): class BrainSegmenterDialog(DeepLearningSegmenterDialog): - def __init__(self, parent, close_on_completion=False): + def __init__(self, parent, auto_segment=False): super().__init__( parent=parent, title=_("Brain segmentation"), @@ -414,7 +417,7 @@ def __init__(self, parent, close_on_completion=False): has_plaidml=True, has_theano=True, segmenter=segment.BrainSegmentProcess, - close_on_completion=close_on_completion, + auto_segment=auto_segment, ) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index ed9063e1e..1a8869c1d 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -434,7 +434,9 @@ def __init__(self, parent, nav_hub): self.remove_non_visible_checkbox.Disable() # Checkbox for brain segmentation - self.brain_segmentation_checkbox = wx.CheckBox(self, label="Brain segmentation") + self.brain_segmentation_checkbox = wx.CheckBox( + self, label="Brain segmentation (~ a few minutes)" + ) top_sizer.Add(self.brain_segmentation_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) # Add create surface button @@ -669,7 +671,7 @@ def SegmentBrain(self): or deep_learning_seg_dialog.HAS_TORCH ): segmentation_dlg = deep_learning_seg_dialog.BrainSegmenterDialog( - self, close_on_completion=True + self, auto_segment=True ) segmentation_dlg.CenterOnScreen() segmentation_dlg.Show() From 6f3bd3936980d680cf3f17a75a434930a8e7f484 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Tue, 25 Feb 2025 16:00:18 +0200 Subject: [PATCH 11/16] MOD: Integrate 'remove non-visible faces'-plugin into InVesalius and add it to the menu under Tools > Surface --- invesalius/constants.py | 1 + invesalius/data/polydata_utils.py | 110 ++++++++++++++++++++++++++++- invesalius/data/surface.py | 27 +++++++- invesalius/gui/dialogs.py | 111 ++++++++++++++++++++++++++++++ invesalius/gui/frame.py | 8 +++ invesalius/gui/task_navigator.py | 14 +--- invesalius/plugins.py | 44 +----------- 7 files changed, 257 insertions(+), 58 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index 615e5aecb..28a9737b0 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -618,6 +618,7 @@ ID_DENSITY_MEASURE = wx.NewIdRef() ID_MASK_DENSITY_MEASURE = wx.NewIdRef() ID_CREATE_SURFACE = wx.NewIdRef() +ID_REMOVE_NON_VISIBLE_FACES = wx.NewIdRef() ID_CREATE_MASK = wx.NewIdRef() ID_MASK_3D_PREVIEW = wx.NewIdRef() ID_MASK_3D_RELOAD = wx.NewIdRef() diff --git a/invesalius/data/polydata_utils.py b/invesalius/data/polydata_utils.py index 6e3199df3..b0e26298c 100644 --- a/invesalius/data/polydata_utils.py +++ b/invesalius/data/polydata_utils.py @@ -20,20 +20,33 @@ import sys from typing import Iterable, List -from vtkmodules.vtkCommonDataModel import vtkPolyData +import numpy as np +from vtkmodules.util import numpy_support +from vtkmodules.vtkCommonCore import vtkIdList, vtkIdTypeArray +from vtkmodules.vtkCommonDataModel import vtkPolyData, vtkSelection, vtkSelectionNode from vtkmodules.vtkFiltersCore import ( vtkAppendPolyData, vtkCleanPolyData, + vtkIdFilter, vtkMassProperties, vtkPolyDataConnectivityFilter, vtkQuadricDecimation, vtkSmoothPolyDataFilter, vtkTriangleFilter, ) +from vtkmodules.vtkFiltersExtraction import vtkExtractSelection +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter from vtkmodules.vtkFiltersModeling import vtkFillHolesFilter from vtkmodules.vtkIOGeometry import vtkOBJReader, vtkSTLReader from vtkmodules.vtkIOPLY import vtkPLYReader from vtkmodules.vtkIOXML import vtkXMLPolyDataReader, vtkXMLPolyDataWriter +from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper, + vtkRenderer, + vtkRenderWindow, + vtkSelectVisiblePoints, +) import invesalius.constants as const import invesalius.data.vtk_utils as vu @@ -262,3 +275,98 @@ def SplitDisconectedParts(polydata: vtkPolyData) -> List[vtkPolyData]: UpdateProgress(region, _("Splitting disconnected regions...")) return polydata_collection + + +def RemoveNonVisibleFaces( + polydata, + positions=[[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]], + remove_visible=False, +): + polydata.BuildLinks() + + mapper = vtkPolyDataMapper() + mapper.SetInputData(polydata) + mapper.Update() + + actor = vtkActor() + actor.SetMapper(mapper) + + renderer = vtkRenderer() + renderer.AddActor(actor) + + render_window = vtkRenderWindow() + render_window.AddRenderer(renderer) + render_window.SetSize(800, 800) + render_window.OffScreenRenderingOn() + + camera = renderer.GetActiveCamera() + renderer.ResetCamera() + + pos = np.array(camera.GetPosition()) + fp = np.array(camera.GetFocalPoint()) + v = pos - fp + mag = np.linalg.norm(v) + vn = v / mag + + id_filter = vtkIdFilter() + id_filter.SetInputData(polydata) + id_filter.PointIdsOn() + id_filter.Update() + + set_points = None + + for position in positions: + pos = fp + np.array(position) * mag + camera.SetPosition(pos.tolist()) + renderer.ResetCamera() + render_window.Render() + + select_visible_points = vtkSelectVisiblePoints() + select_visible_points.SetInputData(id_filter.GetOutput()) + select_visible_points.SetRenderer(renderer) + select_visible_points.Update() + output = select_visible_points.GetOutput() + id_points = numpy_support.vtk_to_numpy( + output.GetPointData().GetAbstractArray("vtkIdFilter_Ids") + ) + if set_points is None: + set_points = set(id_points.tolist()) + else: + set_points.update(id_points.tolist()) + + if remove_visible: + set_points = set(range(polydata.GetNumberOfPoints())) - set_points + cells_ids = set() + for p_id in set_points: + id_list = vtkIdList() + polydata.GetPointCells(p_id, id_list) + for i in range(id_list.GetNumberOfIds()): + cells_ids.add(id_list.GetId(i)) + + try: + id_list = numpy_support.numpy_to_vtkIdTypeArray(np.array(list(cells_ids), dtype=np.int64)) + except ValueError: + id_list = vtkIdTypeArray() + + selection_node = vtkSelectionNode() + selection_node.SetFieldType(vtkSelectionNode.CELL) + selection_node.SetContentType(vtkSelectionNode.INDICES) + selection_node.SetSelectionList(id_list) + + selection = vtkSelection() + selection.AddNode(selection_node) + + extract_selection = vtkExtractSelection() + extract_selection.SetInputData(0, polydata) + extract_selection.SetInputData(1, selection) + extract_selection.Update() + + geometry_filter = vtkGeometryFilter() + geometry_filter.SetInputData(extract_selection.GetOutput()) + geometry_filter.Update() + + clean_polydata = vtkCleanPolyData() + clean_polydata.SetInputData(geometry_filter.GetOutput()) + clean_polydata.Update() + + return clean_polydata.GetOutput() diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index b1fe586f2..405509057 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -73,6 +73,7 @@ from invesalius.data.converters import convert_custom_bin_to_vtk from invesalius.gui import dialogs from invesalius.i18n import tr as _ +from invesalius.utils import new_name_by_pattern # TODO: Verificar ReleaseDataFlagOn and SetSource @@ -216,6 +217,7 @@ def __bind_events(self): # ---- Publisher.subscribe(self.OnSplitSurface, "Split surface") Publisher.subscribe(self.OnLargestSurface, "Create surface from largest region") + Publisher.subscribe(self.OnRemoveNonVisibleFaces, "Remove non-visible faces") Publisher.subscribe(self.OnSeedSurface, "Create surface from seeds") Publisher.subscribe(self.GetBrainSurfaceActor, "Get brain surface actor") @@ -310,7 +312,7 @@ def OnSplitSurface(self): Publisher.sendMessage("Show multiple surfaces", index_list=index_list, visibility=True) - def OnLargestSurface(self): + def OnLargestSurface(self, overwrite=False): """ Create a new surface, based on largest part of the last selected surface. @@ -322,10 +324,31 @@ def OnLargestSurface(self): surface = proj.surface_dict[index] new_polydata = pu.SelectLargestPart(surface.polydata) - new_index = self.CreateSurfaceFromPolydata(new_polydata) + new_index = self.CreateSurfaceFromPolydata(new_polydata, overwrite=overwrite) Publisher.sendMessage("Show single surface", index=new_index, visibility=True) progress_dialog.Close() + def OnRemoveNonVisibleFaces(self): + progress_dialog = dialogs.RemoveNonVisibleFacesProgressWindow() + progress_dialog.Update() + + proj = prj.Project() + index = self.last_surface_index + surface = proj.surface_dict[index] + new_polydata = pu.RemoveNonVisibleFaces(surface.polydata) + + name = new_name_by_pattern(f"{surface.name}_removed_nonvisible") + overwrite = True + + Publisher.sendMessage( + "Create surface from polydata", + polydata=new_polydata, + name=name, + overwrite=overwrite, + ) + Publisher.sendMessage("Fold surface task") + progress_dialog.Close() + def OnImportCustomBinFile(self, filename): import os diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 63b924107..2a27e9b00 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -103,6 +103,7 @@ import invesalius.session as ses import invesalius.utils as utils from invesalius import inv_paths +from invesalius.gui.utils import calc_width_needed from invesalius.gui.widgets.clut_imagedata import EVT_CLUT_NODE_CHANGED, CLUTImageDataWidget from invesalius.gui.widgets.fiducial_buttons import OrderedFiducialButtons from invesalius.gui.widgets.inv_spinctrl import InvFloatSpinCtrl, InvSpinCtrl @@ -6200,6 +6201,116 @@ def Close(self) -> None: self.Destroy() +class RemoveNonVisibleFacesDialog(wx.Dialog): + def __init__(self, parent): + import invesalius.project as prj + + super().__init__( + parent, + title="Remove non-visible faces", + style=wx.DEFAULT_DIALOG_STYLE | wx.FRAME_FLOAT_ON_PARENT, + ) + self.project = prj.Project() + self._init_gui() + self._bind_events() + self._bind_ps_events() + + def _init_gui(self): + self.surfaces_combo = wx.ComboBox(self, -1, style=wx.CB_READONLY) + self.surfaces_combo.SetMinClientSize((calc_width_needed(self.surfaces_combo, 25), -1)) + self.overwrite_check = wx.CheckBox(self, -1, "Overwrite surface") + self.remove_visible_check = wx.CheckBox(self, -1, "Remove visible faces") + self.apply_button = wx.Button(self, wx.ID_APPLY, "Apply") + close_button = wx.Button(self, wx.ID_CLOSE, "Close") + + self.fill_surfaces_combo() + + combo_sizer = wx.BoxSizer(wx.HORIZONTAL) + combo_sizer.Add( + wx.StaticText(self, -1, "Surface"), + 0, + wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.RIGHT, + 5, + ) + combo_sizer.Add(self.surfaces_combo, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) + + button_sizer = wx.StdDialogButtonSizer() + button_sizer.AddButton(self.apply_button) + button_sizer.AddButton(close_button) + button_sizer.Realize() + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(combo_sizer, 0, wx.EXPAND | wx.ALL, 5) + sizer.Add(self.overwrite_check, 0, wx.EXPAND | wx.ALL, 5) + sizer.Add(self.remove_visible_check, 0, wx.EXPAND | wx.ALL, 5) + sizer.Add(button_sizer, 0, wx.EXPAND | wx.ALL, 5) + + self.SetSizerAndFit(sizer) + + def _bind_events(self): + self.Bind(wx.EVT_BUTTON, self.on_apply, id=wx.ID_APPLY) + self.Bind(wx.EVT_BUTTON, self.on_quit, id=wx.ID_CLOSE) + self.Bind(wx.EVT_CLOSE, self.on_quit) + + def _bind_ps_events(self): + Publisher.subscribe(self.on_update_surfaces, "Update surface info in GUI") + Publisher.subscribe(self.on_update_surfaces, "Remove surfaces") + Publisher.subscribe(self.on_update_surfaces, "Change surface name") + + def fill_surfaces_combo(self): + choices = [i.name for i in self.project.surface_dict.values()] + try: + initial_value = choices[0] + enable = True + except IndexError: + initial_value = "" + enable = False + + self.surfaces_combo.SetItems(choices) + self.surfaces_combo.SetValue(initial_value) + self.apply_button.Enable(enable) + + def on_quit(self, evt): + self.Destroy() + + def on_apply(self, evt): + idx = self.surfaces_combo.GetSelection() + surface = list(self.project.surface_dict.values())[idx] + remove_visible = self.remove_visible_check.GetValue() + overwrite = self.overwrite_check.GetValue() + progress_dialog = RemoveNonVisibleFacesProgressWindow() + progress_dialog.Update() + + new_polydata = pu.RemoveNonVisibleFaces(surface.polydata, remove_visible=remove_visible) + if overwrite: + name = surface.name + colour = surface.colour + index = surface.index + else: + name = utils.new_name_by_pattern(f"{surface.name}_removed_nonvisible") + colour = None + index = None + Publisher.sendMessage( + "Create surface from polydata", + polydata=new_polydata, + name=name, + overwrite=overwrite, + index=idx, + colour=colour, + ) + Publisher.sendMessage("Fold surface task") + progress_dialog.Close() + self.on_quit(self) + + def on_update_surfaces(self, *args, **kwargs): + last_idx = self.surfaces_combo.GetSelection() + self.fill_surfaces_combo() + if last_idx < len(self.surfaces_combo.GetItems()): + self.surfaces_combo.SetSelection(last_idx) + else: + self.surfaces_combo.SetSelection(0) + + class GoToDialogScannerCoord(wx.Dialog): def __init__(self, title: str = _("Go to scanner coord...")): wx.Dialog.__init__( diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index fa65b8e7f..ed093cec1 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -691,6 +691,10 @@ def OnMenuClick(self, evt): elif id == const.ID_CREATE_SURFACE: Publisher.sendMessage("Open create surface dialog") + elif id == const.ID_REMOVE_NON_VISIBLE_FACES: + dialog = dlg.RemoveNonVisibleFacesDialog(self) + dialog.Show() + elif id == const.ID_CREATE_MASK: Publisher.sendMessage("New mask from shortcut") @@ -1278,6 +1282,10 @@ def __init_items(self): self.create_surface = surface_menu.Append(const.ID_CREATE_SURFACE, ("New\tCtrl+Shift+C")) self.create_surface.Enable(False) + self.remove_non_visible = surface_menu.Append( + const.ID_REMOVE_NON_VISIBLE_FACES, _("Remove non-visible faces") + ) + # Image menu image_menu = wx.Menu() diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 1a8869c1d..de7ea741e 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -429,9 +429,7 @@ def __init__(self, parent, nav_hub): self.remove_non_visible_checkbox = wx.CheckBox(self, label="Remove non-visible faces") top_sizer.Add(self.remove_non_visible_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) top_sizer.AddSpacer(5) - - # Disable the checkbox by default and enable it only if the plugin is found - self.remove_non_visible_checkbox.Disable() + self.remove_non_visible_checkbox.SetValue(True) # Checkbox for brain segmentation self.brain_segmentation_checkbox = wx.CheckBox( @@ -470,7 +468,6 @@ def __bind_events(self): Publisher.subscribe(self.SetThresholdValues2, "Set threshold values") Publisher.subscribe(self.SelectMaskName, "Select mask name in combo") Publisher.subscribe(self.SetItemsColour, "Set GUI items colour") - Publisher.subscribe(self.OnPluginsFound, "Add plugins menu items") Publisher.subscribe(self.OnRemoveMasks, "Remove masks") Publisher.subscribe(self.AddMask, "Add mask") @@ -527,13 +524,6 @@ def OnSlideChanging(self, evt): session = ses.Session() session.ChangeProject() - def OnPluginsFound(self, items): - for item in items: - if item == "Remove non-visible faces": - self.remove_non_visible_checkbox.Enable() - self.remove_non_visible_checkbox.SetValue(True) - break - def SetItemsColour(self, colour): self.gradient.SetColour(colour) @@ -637,7 +627,7 @@ def CreateSurface(self, evt): dlg.InexistentMask() def SelectLargestSurface(self): - Publisher.sendMessage("Create surface from largest region") + Publisher.sendMessage("Create surface from largest region", overwrite=True) def RemoveNonVisibleFaces(self): Publisher.sendMessage("Remove non-visible faces") diff --git a/invesalius/plugins.py b/invesalius/plugins.py index 0378c6fc1..05af7ba4c 100644 --- a/invesalius/plugins.py +++ b/invesalius/plugins.py @@ -26,10 +26,8 @@ from types import ModuleType from typing import TYPE_CHECKING -from invesalius import inv_paths, project -from invesalius.gui import dialogs +from invesalius import inv_paths from invesalius.pubsub import pub as Publisher -from invesalius.utils import new_name_by_pattern if TYPE_CHECKING: import os @@ -53,7 +51,6 @@ def __init__(self): def __bind_pubsub_evt(self) -> None: Publisher.subscribe(self.load_plugin, "Load plugin") - Publisher.subscribe(self.remove_non_visible_faces_no_gui, "Remove non-visible faces") def find_plugins(self) -> None: self.plugins = {} @@ -90,42 +87,3 @@ def load_plugin(self, plugin_name: str) -> None: sys.modules[plugin_name] = plugin_module main = importlib.import_module(plugin_name + ".main") main.load() - - # Remove non-visible faces from a surface without using the plugin GUI. - # Defaults to the last surface in surface_dict which is generally the newest surface - def remove_non_visible_faces_no_gui(self, surface_idx: int = -1) -> None: - plugin_name = "Remove non-visible faces" - if plugin_name in self.plugins: - progress_dialog = dialogs.RemoveNonVisibleFacesProgressWindow() - progress_dialog.Update() - plugin_module = import_source( - plugin_name, self.plugins[plugin_name]["folder"].joinpath("__init__.py") - ) - sys.modules[plugin_name] = plugin_module - remove_faces = importlib.import_module(plugin_name + ".remove_non_visible_faces") - - inv_proj = project.Project() - try: - surface = list(inv_proj.surface_dict.values())[surface_idx] - except IndexError: - print(f"Invalid surface_dict index {surface_idx}, did not remove non-visible faces") - return - - overwrite = False - new_polydata = remove_faces.remove_non_visible_faces( - surface.polydata, remove_visible=False - ) - - name = new_name_by_pattern(f"{surface.name}_removed_nonvisible") - colour = None - - Publisher.sendMessage( - "Create surface from polydata", - polydata=new_polydata, - name=name, - overwrite=overwrite, - index=surface_idx, - colour=colour, - ) - Publisher.sendMessage("Fold surface task") - progress_dialog.Close() From 79e42daafd383aeea7cbdcc81b1a37dc2a76db07 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Tue, 25 Feb 2025 17:14:04 +0200 Subject: [PATCH 12/16] MOD: name created surfaces as scalp or brain --- invesalius/data/surface.py | 7 +++++-- invesalius/gui/task_navigator.py | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index 405509057..5501ac110 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -312,7 +312,7 @@ def OnSplitSurface(self): Publisher.sendMessage("Show multiple surfaces", index_list=index_list, visibility=True) - def OnLargestSurface(self, overwrite=False): + def OnLargestSurface(self, overwrite=False, name=""): """ Create a new surface, based on largest part of the last selected surface. @@ -324,11 +324,14 @@ def OnLargestSurface(self, overwrite=False): surface = proj.surface_dict[index] new_polydata = pu.SelectLargestPart(surface.polydata) - new_index = self.CreateSurfaceFromPolydata(new_polydata, overwrite=overwrite) + new_index = self.CreateSurfaceFromPolydata(new_polydata, overwrite=overwrite, name=name) Publisher.sendMessage("Show single surface", index=new_index, visibility=True) progress_dialog.Close() def OnRemoveNonVisibleFaces(self): + """ + Create a new surface where non-visible faces have been removed. + """ progress_dialog = dialogs.RemoveNonVisibleFacesProgressWindow() progress_dialog.Update() diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index de7ea741e..e6ca64df6 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -558,7 +558,7 @@ def CreateBrainSurface(self): method = {"algorithm": algorithm, "options": options} srf_options = { "index": mask_index, - "name": "", + "name": "Brain", "quality": _("Optimal *"), "fill": False, "keep_largest": False, @@ -612,7 +612,7 @@ def CreateSurface(self, evt): method = {"algorithm": algorithm, "options": options} srf_options = { "index": mask_index, - "name": "", + "name": "Scalp", "quality": _("Optimal *"), "fill": False, "keep_largest": False, @@ -627,7 +627,7 @@ def CreateSurface(self, evt): dlg.InexistentMask() def SelectLargestSurface(self): - Publisher.sendMessage("Create surface from largest region", overwrite=True) + Publisher.sendMessage("Create surface from largest region", overwrite=True, name="Scalp") def RemoveNonVisibleFaces(self): Publisher.sendMessage("Remove non-visible faces") From 4153030556f9192cf2e4784b9eb8e011d6ba5309 Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Tue, 25 Feb 2025 17:23:41 +0200 Subject: [PATCH 13/16] MOD: keep the largest surface when creating a brain surface --- invesalius/gui/task_navigator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index e6ca64df6..6f5b64e7f 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -561,7 +561,7 @@ def CreateBrainSurface(self): "name": "Brain", "quality": _("Optimal *"), "fill": False, - "keep_largest": False, + "keep_largest": True, "overwrite": False, } Publisher.sendMessage( From 4eebbe3f7b28c146582e80c88d7944261666ab6f Mon Sep 17 00:00:00 2001 From: Henrik Kauppi Date: Fri, 28 Feb 2025 20:23:07 +0200 Subject: [PATCH 14/16] ADD: Imports page to the coregistration panel for easier image and project loading --- invesalius/constants.py | 13 +- invesalius/gui/default_tasks.py | 2 +- invesalius/gui/task_navigator.py | 222 ++++++++++++++++++++++++++++++- 3 files changed, 226 insertions(+), 11 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index 28a9737b0..8cfbe62c9 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -823,12 +823,13 @@ # Page order in the coregistration panel -HEAD_PAGE = 0 -IMAGE_PAGE = 1 -TRACKER_PAGE = 2 -REFINE_PAGE = 3 -STYLUS_PAGE = 4 -STIMULATOR_PAGE = 5 +IMPORTS_PAGE = 0 +HEAD_PAGE = 1 +IMAGE_PAGE = 2 +TRACKER_PAGE = 3 +REFINE_PAGE = 4 +STYLUS_PAGE = 5 +STIMULATOR_PAGE = 6 # ------------ Navigation defaults ------------------- diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index da887f13f..1f8e48b63 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -104,7 +104,7 @@ def GetExpandedIconImage(): # Main panel class Panel(wx.Panel): def __init__(self, parent): - wx.Panel.__init__(self, parent, pos=wx.Point(5, 5), size=wx.Size(350, 656)) + wx.Panel.__init__(self, parent, pos=wx.Point(5, 5), size=wx.Size(385, 656)) # sizer = wx.BoxSizer(wx.VERTICAL) gbs = wx.GridBagSizer(5, 5) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 6f5b64e7f..0460f44ef 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -42,6 +42,11 @@ import wx.lib.platebtn as pbtn from wx.lib.mixins.listctrl import ColumnSorterMixin +try: + import wx.lib.agw.hyperlink as hl +except ImportError: + import wx.lib.hyperlink as hl + import invesalius.constants as const import invesalius.data.coordinates as dco import invesalius.data.slice_ as slice_ @@ -321,6 +326,7 @@ def __init__(self, parent, nav_hub): self.tracker = nav_hub.tracker self.image = nav_hub.image + book.AddPage(ImportsPage(book, nav_hub), _("Imports")) book.AddPage(HeadPage(book, nav_hub), _("Head")) book.AddPage(ImagePage(book, nav_hub), _("Image")) book.AddPage(TrackerPage(book, nav_hub), _("Patient")) @@ -328,7 +334,14 @@ def __init__(self, parent, nav_hub): book.AddPage(StylusPage(book, nav_hub), _("Stylus")) book.AddPage(StimulatorPage(book, nav_hub), _("TMS Coil")) - book.SetSelection(0) + session = ses.Session() + project_status = session.GetConfig("project_status") + + # Show the head page by default if there is a project loaded + if project_status != const.PROJECT_STATUS_CLOSED: + book.SetSelection(const.HEAD_PAGE) + else: + book.SetSelection(const.IMPORTS_PAGE) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(book, 0, wx.EXPAND) @@ -339,6 +352,7 @@ def __init__(self, parent, nav_hub): self.__bind_events() def __bind_events(self): + Publisher.subscribe(self._FoldImports, "Move to imports page") Publisher.subscribe(self._FoldHead, "Move to head model page") Publisher.subscribe(self._FoldTracker, "Move to tracker page") Publisher.subscribe(self._FoldRefine, "Move to refine page") @@ -372,6 +386,9 @@ def OnPageChanged(self, evt): wx.MessageBox(_("Please do the tracker registration first."), _("InVesalius 3")) # Unfold specific notebook pages + def _FoldImports(self): + self.book.SetSelection(const.IMPORTS_PAGE) + def _FoldHead(self): self.book.SetSelection(const.HEAD_PAGE) @@ -392,13 +409,203 @@ def _FoldStimulator(self): self.book.SetSelection(const.STIMULATOR_PAGE) +class ImportsPage(wx.Panel): + def __init__(self, parent, nav_hub): + wx.Panel.__init__(self, parent) + + background_colour = wx.Colour(255, 255, 255) + self.SetBackgroundColour(background_colour) + + self.navigation = nav_hub + self.BTN_IMPORT_LOCAL_NAV = wx.NewIdRef() + self.BTN_OPEN_PROJECT_NAV = wx.NewIdRef() + self.BTN_IMPORT_NIFTI_NAV = wx.NewIdRef() + self.BTN_NEXT = wx.NewIdRef() + + self.top_sizer = wx.BoxSizer(wx.VERTICAL) + self.bottom_sizer = wx.BoxSizer(wx.VERTICAL) + self.main_sizer = wx.BoxSizer(wx.VERTICAL) + + # Counter for projects loaded in current GUI + self.proj_count = 0 + + # Fixed hyperlink items + tooltip = _("Select DICOM files to be reconstructed") + link_import_local = hl.HyperLinkCtrl(self, -1, _("Import DICOM images...")) + link_import_local.SetUnderlines(False, False, False) + link_import_local.SetBold(True) + link_import_local.SetColours("BLACK", "BLACK", "BLACK") + link_import_local.SetBackgroundColour(background_colour) + link_import_local.SetToolTip(tooltip) + link_import_local.AutoBrowse(False) + link_import_local.UpdateLink() + link_import_local.Bind(hl.EVT_HYPERLINK_LEFT, self.OnLinkImport) + + tooltip = _("Select NIFTI files to be reconstructed") + link_import_nifti = hl.HyperLinkCtrl(self, -1, _("Import NIFTI images...")) + link_import_nifti.SetUnderlines(False, False, False) + link_import_nifti.SetBold(True) + link_import_nifti.SetColours("BLACK", "BLACK", "BLACK") + link_import_nifti.SetBackgroundColour(background_colour) + link_import_nifti.SetToolTip(tooltip) + link_import_nifti.AutoBrowse(False) + link_import_nifti.UpdateLink() + link_import_nifti.Bind(hl.EVT_HYPERLINK_LEFT, self.OnLinkImportNifti) + + tooltip = _("Open an existing InVesalius project...") + link_open_proj = hl.HyperLinkCtrl(self, -1, _("Open an existing project...")) + link_open_proj.SetUnderlines(False, False, False) + link_open_proj.SetBold(True) + link_open_proj.SetColours("BLACK", "BLACK", "BLACK") + link_open_proj.SetBackgroundColour(background_colour) + link_open_proj.SetToolTip(tooltip) + link_open_proj.AutoBrowse(False) + link_open_proj.UpdateLink() + link_open_proj.Bind(hl.EVT_HYPERLINK_LEFT, self.OnLinkOpenProject) + + # Images for buttons + BMP_IMPORT = wx.Bitmap( + str(inv_paths.ICON_DIR.joinpath("file_import_original.png")), wx.BITMAP_TYPE_PNG + ) + BMP_OPEN_PROJECT = wx.Bitmap( + str(inv_paths.ICON_DIR.joinpath("file_open_original.png")), wx.BITMAP_TYPE_PNG + ) + + # Buttons related to hyperlinks + button_style = pbtn.PB_STYLE_SQUARE | pbtn.PB_STYLE_DEFAULT + + button_import_local = pbtn.PlateButton( + self, self.BTN_IMPORT_LOCAL_NAV, "", BMP_IMPORT, style=button_style + ) + button_import_local.SetBackgroundColour(self.GetBackgroundColour()) + button_import_nifti = pbtn.PlateButton( + self, self.BTN_IMPORT_NIFTI_NAV, "", BMP_IMPORT, style=button_style + ) + button_import_nifti.SetBackgroundColour(self.GetBackgroundColour()) + button_open_proj = pbtn.PlateButton( + self, self.BTN_OPEN_PROJECT_NAV, "", BMP_OPEN_PROJECT, style=button_style + ) + button_open_proj.SetBackgroundColour(self.GetBackgroundColour()) + + # Next button + next_button = wx.Button(self, id=self.BTN_NEXT, label="Next") + self.bottom_sizer.Add(next_button, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.BOTTOM, 10) + + # When using PlaneButtons, it is necessary to bind events from parent window + self.Bind(wx.EVT_BUTTON, self.OnButton) + + # Tags and grid sizer for fixed items + flag_link = wx.EXPAND | wx.GROW | wx.LEFT | wx.TOP + flag_button = wx.EXPAND | wx.GROW + + fixed_sizer = wx.FlexGridSizer(rows=3, cols=2, hgap=2, vgap=0) + fixed_sizer.AddGrowableCol(0, 1) + fixed_sizer.AddMany( + [ + (link_import_local, 1, flag_link, 3), + (button_import_local, 0, flag_button), + (link_import_nifti, 3, flag_link, 3), + (button_import_nifti, 0, flag_button), + (link_open_proj, 5, flag_link, 3), + (button_open_proj, 0, flag_button), + ] + ) + + # Add top and bottom sizers to the main sizer + self.top_sizer.Add(fixed_sizer, 0, wx.GROW | wx.EXPAND) + self.main_sizer.Add(self.top_sizer, 0, wx.GROW | wx.EXPAND) + self.main_sizer.AddStretchSpacer() + self.main_sizer.Add(self.bottom_sizer, 0, wx.GROW | wx.EXPAND) + + # Update main sizer and panel layout + self.SetSizer(self.main_sizer) + self.Update() + self.SetAutoLayout(1) + self.sizer = self.main_sizer + + # Load a list of recent projects + self.LoadRecentProjects() + + def OnLinkOpenProject(self, event): + self.OpenProject() + event.Skip() + + def OpenProject(self, path=None): + if path: + Publisher.sendMessage("Open recent project", filepath=path) + else: + Publisher.sendMessage("Show open project dialog") + + def OnLinkImport(self, event): + self.ImportDicom() + event.Skip() + + def ImportDicom(self): + Publisher.sendMessage("Show import directory dialog") + + def OnLinkImportNifti(self, event): + self.ImportNifti() + event.Skip() + + def ImportNifti(self): + Publisher.sendMessage("Show import other files dialog", id_type=const.ID_NIFTI_IMPORT) + + def OnButton(self, evt): + id = evt.GetId() + + if id == self.BTN_NEXT: + Publisher.sendMessage("Move to head model page") + elif id == self.BTN_IMPORT_LOCAL_NAV: + self.ImportDicom() + elif id == self.BTN_IMPORT_NIFTI_NAV: + self.ImportNifti() + elif id == self.BTN_OPEN_PROJECT_NAV: + self.OpenProject() + + # Add a list of recent projects to the Imports page of the navigation panel + def LoadRecentProjects(self): + import invesalius.session as ses + + session = ses.Session() + recent_projects = session.GetConfig("recent_projects") + + for path, filename in recent_projects: + self.LoadProject(filename, path) + + def LoadProject(self, proj_name="Unnamed", proj_dir=""): + """ + Create a hyperlink for the project, and add it to the list of recent projects + on the Imports page of the navigation panel. The list is capped at 3 projects. + """ + proj_path = os.path.join(proj_dir, proj_name) + + if self.proj_count < 3: + self.proj_count += 1 + + # Create labels + label = " " + str(self.proj_count) + ". " + proj_name + + # Create corresponding hyperlink + proj_link = hl.HyperLinkCtrl(self, -1, label) + proj_link.SetUnderlines(False, False, False) + proj_link.SetColours("BLACK", "BLACK", "BLACK") + proj_link.SetBackgroundColour(self.GetBackgroundColour()) + proj_link.AutoBrowse(False) + proj_link.UpdateLink() + proj_link.Bind(hl.EVT_HYPERLINK_LEFT, lambda e: self.OpenProject(proj_path)) + + # Add the link to the sizer and to the hyperlinks list + self.top_sizer.Add(proj_link, 1, wx.GROW | wx.EXPAND | wx.ALL, 2) + self.Update() + + class HeadPage(wx.Panel): def __init__(self, parent, nav_hub): wx.Panel.__init__(self, parent) # Create sizers top_sizer = wx.BoxSizer(wx.VERTICAL) - bottom_sizer = wx.BoxSizer(wx.VERTICAL) + bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) main_sizer = wx.BoxSizer(wx.VERTICAL) # Add label above combo box @@ -443,10 +650,14 @@ def __init__(self, parent, nav_hub): top_sizer.AddStretchSpacer() top_sizer.Add(create_head_button, 0, wx.ALIGN_CENTER) - # Add next button + # Add next and back buttons + back_button = wx.Button(self, label="Back") + back_button.Bind(wx.EVT_BUTTON, partial(self.OnBack)) + bottom_sizer.Add(back_button, 0, wx.LEFT, 10) + bottom_sizer.AddStretchSpacer() next_button = wx.Button(self, label="Next") next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) - bottom_sizer.Add(next_button, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10) + bottom_sizer.Add(next_button, 0, wx.RIGHT, 10) # Main sizer config main_sizer.Add(top_sizer, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 10) @@ -461,6 +672,9 @@ def __init__(self, parent, nav_hub): def OnNext(self, evt): Publisher.sendMessage("Move to image page") + def OnBack(self, evt): + Publisher.sendMessage("Move to imports page") + def __bind_events(self): Publisher.subscribe(self.OnSuccessfulBrainSegmentation, "Brain segmentation completed") Publisher.subscribe(self.SetThresholdBounds, "Update threshold limits") From 70224a26d603ef91aa5819d752cf513bc56511dd Mon Sep 17 00:00:00 2001 From: rmatsuda Date: Tue, 4 Mar 2025 14:16:33 +0200 Subject: [PATCH 15/16] ADD: smooth surface and FIX: small bugs --- invesalius/data/polydata_utils.py | 12 +++++--- invesalius/data/surface.py | 16 ++++++++++ invesalius/gui/default_tasks.py | 5 +++- invesalius/gui/dialogs.py | 19 ++++++++++++ invesalius/gui/task_navigator.py | 50 +++++++++++++++++++++++++++---- 5 files changed, 91 insertions(+), 11 deletions(-) diff --git a/invesalius/data/polydata_utils.py b/invesalius/data/polydata_utils.py index b0e26298c..1ddfb2d6d 100644 --- a/invesalius/data/polydata_utils.py +++ b/invesalius/data/polydata_utils.py @@ -96,14 +96,18 @@ def ApplySmoothFilter( smoother.SetNumberOfIterations(iterations) smoother.SetFeatureAngle(80) smoother.SetRelaxationFactor(relaxation_factor) - smoother.FeatureEdgeSmoothingOn() - smoother.BoundarySmoothingOn() - smoother.GetOutput().ReleaseDataFlagOn() + smoother.FeatureEdgeSmoothingOff() + smoother.BoundarySmoothingOff() + smoother.Update() + filler = vtkFillHolesFilter() + filler.SetInputConnection(smoother.GetOutputPort()) + filler.SetHoleSize(1000) + filler.Update() smoother.AddObserver( "ProgressEvent", lambda obj, evt: UpdateProgress(smoother, "Smoothing surface...") ) - return smoother.GetOutput() + return filler.GetOutput() def FillSurfaceHole(polydata: vtkPolyData) -> "vtkPolyData": diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index 5501ac110..682632658 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -219,6 +219,7 @@ def __bind_events(self): Publisher.subscribe(self.OnLargestSurface, "Create surface from largest region") Publisher.subscribe(self.OnRemoveNonVisibleFaces, "Remove non-visible faces") Publisher.subscribe(self.OnSeedSurface, "Create surface from seeds") + Publisher.subscribe(self.OnSmoothSurface, "Create smooth surface") Publisher.subscribe(self.GetBrainSurfaceActor, "Get brain surface actor") Publisher.subscribe(self.OnDuplicate, "Duplicate surfaces") @@ -294,6 +295,21 @@ def OnSeedSurface(self, seeds): Publisher.sendMessage("Show single surface", index=index, visibility=True) # self.ShowActor(index, True) + def OnSmoothSurface(self, overwrite=False, name=""): + """ + Create a new smooth surface, based on the last selected surface. + """ + progress_dialog = dialogs.SmoothSurfaceProgressWindow() + progress_dialog.Update() + index = self.last_surface_index + proj = prj.Project() + surface = proj.surface_dict[index] + + new_polydata = pu.ApplySmoothFilter(polydata=surface.polydata, iterations=20, relaxation_factor=0.4) + new_index = self.CreateSurfaceFromPolydata(new_polydata, overwrite=overwrite, name=name) + Publisher.sendMessage("Show single surface", index=new_index, visibility=True) + progress_dialog.Close() + def OnSplitSurface(self): """ Create n new surfaces, based on the last selected surface, diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index 1f8e48b63..d49a040f7 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -375,9 +375,12 @@ def OnEnableState(self, state): self.SetStateProjectClose() def SetStateProjectClose(self): + session = ses.Session() + mode = session.GetConfig("mode") self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) for item in self.enable_items: - item.Disable() + if mode != const.MODE_NAVIGATOR: + item.Disable() def SetStateProjectOpen(self): session = ses.Session() diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 2a27e9b00..559fb5d38 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1998,6 +1998,25 @@ def Close(self) -> None: self.dlg.Destroy() +class SmoothSurfaceProgressWindow: + def __init__(self): + title = "InVesalius 3" + message = "Creating a new smooth surface ..." + style = wx.PD_APP_MODAL | wx.PD_CAN_ABORT + parent = wx.GetApp().GetTopWindow() + self.dlg = wx.ProgressDialog(title, message, parent=parent, style=style) + self.dlg.Show() + + def Update(self, msg: Optional[str] = None, value=None) -> None: + if msg is None: + self.dlg.Pulse() + else: + self.dlg.Pulse(msg) + + def Close(self) -> None: + self.dlg.Destroy() + + class RemoveNonVisibleFacesProgressWindow: def __init__(self): title = "InVesalius 3" diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 0460f44ef..7d3cc9c71 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -231,7 +231,6 @@ def __calc_best_size(self, panel): def OnEnableState(self, state): if not state: self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) - Publisher.sendMessage("Move to image page") def OnShowDbs(self): self.dbs_item.Show() @@ -338,7 +337,7 @@ def __init__(self, parent, nav_hub): project_status = session.GetConfig("project_status") # Show the head page by default if there is a project loaded - if project_status != const.PROJECT_STATUS_CLOSED: + if project_status == const.PROJECT_STATUS_OPENED: book.SetSelection(const.HEAD_PAGE) else: book.SetSelection(const.IMPORTS_PAGE) @@ -359,6 +358,10 @@ def __bind_events(self): Publisher.subscribe(self._FoldStylus, "Move to stylus page") Publisher.subscribe(self._FoldStimulator, "Move to stimulator page") Publisher.subscribe(self._FoldImage, "Move to image page") + Publisher.subscribe(self.OnCloseProject, "Close project data") + + def OnCloseProject(self): + self.book.SetSelection(const.IMPORTS_PAGE) def OnPageChanging(self, evt): # page = evt.GetOldSelection() @@ -368,6 +371,14 @@ def OnPageChanged(self, evt): old_page = evt.GetOldSelection() new_page = evt.GetSelection() + session = ses.Session() + project_status = session.GetConfig("project_status") + if old_page == const.IMPORTS_PAGE and project_status == const.PROJECT_STATUS_CLOSED and new_page != const.IMPORTS_PAGE: + # Do not allow user to move to other (forward) tabs. + self.book.SetSelection(const.IMPORTS_PAGE) + wx.MessageBox(_("Please import image first."), _("InVesalius 3")) + return + # old page validations if old_page <= const.IMAGE_PAGE and new_page > const.IMAGE_PAGE: # Do not allow user to move to other (forward) tabs if image fiducials not done. @@ -535,6 +546,7 @@ def OpenProject(self, path=None): Publisher.sendMessage("Open recent project", filepath=path) else: Publisher.sendMessage("Show open project dialog") + Publisher.sendMessage("Move to head model page") def OnLinkImport(self, event): self.ImportDicom() @@ -542,6 +554,7 @@ def OnLinkImport(self, event): def ImportDicom(self): Publisher.sendMessage("Show import directory dialog") + Publisher.sendMessage("Move to head model page") def OnLinkImportNifti(self, event): self.ImportNifti() @@ -549,6 +562,7 @@ def OnLinkImportNifti(self, event): def ImportNifti(self): Publisher.sendMessage("Show import other files dialog", id_type=const.ID_NIFTI_IMPORT) + Publisher.sendMessage("Move to head model page") def OnButton(self, evt): id = evt.GetId() @@ -627,7 +641,7 @@ def __init__(self, parent, nav_hub): # Checkbox for selecting the largest surface self.select_largest_surface_checkbox = wx.CheckBox(self, label="Select largest surface") - top_sizer.AddStretchSpacer(2) + top_sizer.AddStretchSpacer(1) top_sizer.Add(self.select_largest_surface_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) top_sizer.AddSpacer(5) self.select_largest_surface_checkbox.SetValue(True) @@ -638,6 +652,12 @@ def __init__(self, parent, nav_hub): top_sizer.AddSpacer(5) self.remove_non_visible_checkbox.SetValue(True) + # Checkbox for smooth scalp surface + self.smooth_surface_checkbox = wx.CheckBox(self, label="Smooth scalp surface") + top_sizer.Add(self.smooth_surface_checkbox, 0, wx.ALIGN_LEFT | wx.LEFT, 10) + top_sizer.AddSpacer(5) + self.smooth_surface_checkbox.SetValue(True) + # Checkbox for brain segmentation self.brain_segmentation_checkbox = wx.CheckBox( self, label="Brain segmentation (~ a few minutes)" @@ -684,6 +704,10 @@ def __bind_events(self): Publisher.subscribe(self.SetItemsColour, "Set GUI items colour") Publisher.subscribe(self.OnRemoveMasks, "Remove masks") Publisher.subscribe(self.AddMask, "Add mask") + Publisher.subscribe(self.OnCloseProject, "Close project data") + + def OnCloseProject(self): + self.OnRemoveMasks(list(reversed(range(self.combo_mask.GetCount())))) def __bind_events_wx(self): self.combo_mask.Bind(wx.EVT_COMBOBOX, self.OnComboName) @@ -744,7 +768,8 @@ def SetItemsColour(self, colour): # Creates the head surface from a mask, and depending on the checkboxes # selects the largest surface, removes non-visible faces, and does brain segmentation def OnCreateHeadSurface(self, evt): - self.CreateSurface(evt) + if not self.CreateSurface(evt): + return if self.select_largest_surface_checkbox.IsChecked(): self.SelectLargestSurface() @@ -752,9 +777,16 @@ def OnCreateHeadSurface(self, evt): if self.remove_non_visible_checkbox.IsChecked(): self.RemoveNonVisibleFaces() + if self.smooth_surface_checkbox.IsChecked(): + self.SmoothSurface() + + self.VisualizeScalpSurface() + if self.brain_segmentation_checkbox.IsChecked(): self.SegmentBrain() + Publisher.sendMessage("Move to image page") + def CreateBrainSurface(self): options = {"angle": 0.7, "max distance": 3.0, "min weight": 0.5, "steps": 10} algorithm = "ca_smoothing" @@ -822,13 +854,13 @@ def CreateSurface(self, evt): mask_index = idx break else: - return + return False method = {"algorithm": algorithm, "options": options} srf_options = { "index": mask_index, "name": "Scalp", "quality": _("Optimal *"), - "fill": False, + "fill": True, "keep_largest": False, "overwrite": False, } @@ -837,8 +869,10 @@ def CreateSurface(self, evt): surface_parameters={"method": method, "options": srf_options}, ) Publisher.sendMessage("Fold surface task") + return True else: dlg.InexistentMask() + return False def SelectLargestSurface(self): Publisher.sendMessage("Create surface from largest region", overwrite=True, name="Scalp") @@ -846,6 +880,10 @@ def SelectLargestSurface(self): def RemoveNonVisibleFaces(self): Publisher.sendMessage("Remove non-visible faces") + def SmoothSurface(self): + Publisher.sendMessage("Create smooth surface", overwrite=True, name="Scalp") + + def VisualizeScalpSurface(self): proj = prj.Project() surface_idx = len(proj.surface_dict) - 1 scalp_colour = [255, 235, 255] From 5883d84805923dbc03ad1ea20059923359fab414 Mon Sep 17 00:00:00 2001 From: rmatsuda Date: Tue, 4 Mar 2025 14:17:12 +0200 Subject: [PATCH 16/16] RUFF --- invesalius/data/surface.py | 4 +++- invesalius/gui/task_navigator.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index 682632658..ccb1890ff 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -305,7 +305,9 @@ def OnSmoothSurface(self, overwrite=False, name=""): proj = prj.Project() surface = proj.surface_dict[index] - new_polydata = pu.ApplySmoothFilter(polydata=surface.polydata, iterations=20, relaxation_factor=0.4) + new_polydata = pu.ApplySmoothFilter( + polydata=surface.polydata, iterations=20, relaxation_factor=0.4 + ) new_index = self.CreateSurfaceFromPolydata(new_polydata, overwrite=overwrite, name=name) Publisher.sendMessage("Show single surface", index=new_index, visibility=True) progress_dialog.Close() diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 7d3cc9c71..e1173f33f 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -373,7 +373,11 @@ def OnPageChanged(self, evt): session = ses.Session() project_status = session.GetConfig("project_status") - if old_page == const.IMPORTS_PAGE and project_status == const.PROJECT_STATUS_CLOSED and new_page != const.IMPORTS_PAGE: + if ( + old_page == const.IMPORTS_PAGE + and project_status == const.PROJECT_STATUS_CLOSED + and new_page != const.IMPORTS_PAGE + ): # Do not allow user to move to other (forward) tabs. self.book.SetSelection(const.IMPORTS_PAGE) wx.MessageBox(_("Please import image first."), _("InVesalius 3"))