diff --git a/invesalius/constants.py b/invesalius/constants.py index fcb3f75fc..8cfbe62c9 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() @@ -820,6 +821,16 @@ BRAIN_INTENSITY_MTMS = 8 BRAIN_UUID = 9 +# Page order in the coregistration panel + +IMPORTS_PAGE = 0 +HEAD_PAGE = 1 +IMAGE_PAGE = 2 +TRACKER_PAGE = 3 +REFINE_PAGE = 4 +STYLUS_PAGE = 5 +STIMULATOR_PAGE = 6 + # ------------ Navigation defaults ------------------- MARKER_COLOUR = (1.0, 1.0, 0.0) diff --git a/invesalius/data/polydata_utils.py b/invesalius/data/polydata_utils.py index 6e3199df3..1ddfb2d6d 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 @@ -83,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": @@ -262,3 +279,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 c2822e0e9..ccb1890ff 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,7 +217,9 @@ 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.OnSmoothSurface, "Create smooth surface") Publisher.subscribe(self.GetBrainSurfaceActor, "Get brain surface actor") Publisher.subscribe(self.OnDuplicate, "Duplicate surfaces") @@ -292,6 +295,23 @@ 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, @@ -310,18 +330,45 @@ def OnSplitSurface(self): Publisher.sendMessage("Show multiple surfaces", index_list=index_list, visibility=True) - def OnLargestSurface(self): + def OnLargestSurface(self, overwrite=False, name=""): """ 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] new_polydata = pu.SelectLargestPart(surface.polydata) - new_index = self.CreateSurfaceFromPolydata(new_polydata) + 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() + + 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/deep_learning_seg_dialog.py b/invesalius/gui/deep_learning_seg_dialog.py index 4527aa3e6..82b241235 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, + auto_segment=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.auto_segment = auto_segment self.backends = backends @@ -87,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, @@ -332,6 +343,10 @@ def AfterSegment(self): self.elapsed_time_timer.Stop() self.apply_segment_threshold() + if self.auto_segment: + self.OnClose(self) + Publisher.sendMessage("Brain segmentation completed") + def SetProgress(self, progress): self.progress.SetValue(int(progress * 100)) wx.GetApp().Yield() @@ -361,8 +376,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 +409,7 @@ def ShowProgress(self): class BrainSegmenterDialog(DeepLearningSegmenterDialog): - def __init__(self, parent): + def __init__(self, parent, auto_segment=False): super().__init__( parent=parent, title=_("Brain segmentation"), @@ -401,6 +417,7 @@ def __init__(self, parent): has_plaidml=True, has_theano=True, segmenter=segment.BrainSegmentProcess, + auto_segment=auto_segment, ) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index 958d15ad9..d49a040f7 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) @@ -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") @@ -370,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 e0b0adf02..559fb5d38 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 @@ -1978,6 +1979,63 @@ 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 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" + 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 @@ -6162,6 +6220,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 40cf6bd6f..e1173f33f 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -42,13 +42,21 @@ 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_ 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 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 @@ -223,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() @@ -318,13 +325,22 @@ 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")) book.AddPage(RefinePage(book, nav_hub), _("Refine")) 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_OPENED: + book.SetSelection(const.HEAD_PAGE) + else: + book.SetSelection(const.IMPORTS_PAGE) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(book, 0, wx.EXPAND) @@ -335,11 +351,17 @@ 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") 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() @@ -349,39 +371,568 @@ 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 == 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 _FoldImports(self): + self.book.SetSelection(const.IMPORTS_PAGE) + + 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 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") + Publisher.sendMessage("Move to head model page") + + def OnLinkImport(self, event): + self.ImportDicom() + event.Skip() + + def ImportDicom(self): + Publisher.sendMessage("Show import directory dialog") + Publisher.sendMessage("Move to head model page") + + def OnLinkImportNifti(self, event): + self.ImportNifti() + event.Skip() + + 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() + + 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.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_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") + 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) + + # Checkbox for selecting the largest surface + self.select_largest_surface_checkbox = wx.CheckBox(self, label="Select largest surface") + 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) + + # 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) + 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)" + ) + 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)) + top_sizer.AddStretchSpacer() + top_sizer.Add(create_head_button, 0, wx.ALIGN_CENTER) + + # 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.RIGHT, 10) + + # 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 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") + 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") + 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) + 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_mask.Append(mask.name) + + def SelectMaskName(self, index): + if index >= 0: + self.combo_mask.SetSelection(index) + else: + self.combo_mask.SetValue("") + + def OnRemoveMasks(self, mask_indexes): + for i in mask_indexes: + self.combo_mask.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) + + # 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): + if not self.CreateSurface(evt): + return + + if self.select_largest_surface_checkbox.IsChecked(): + self.SelectLargestSurface() + + 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" + proj = prj.Project() + mask_index = len(proj.mask_dict) - 1 + brain_colour = [235, 245, 255] + + 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": "Brain", + "quality": _("Optimal *"), + "fill": False, + "keep_largest": True, + "overwrite": False, + } + Publisher.sendMessage( + "Create surface from index", + surface_parameters={"method": method, "options": srf_options}, + ) + 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() + + 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: + surface_dlg = dlg.SurfaceDialog() + if surface_dlg.ShowModal() == wx.ID_OK: + algorithm = surface_dlg.GetAlgorithmSelected() + options = surface_dlg.GetOptions() + else: + to_generate = False + surface_dlg.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 False + method = {"algorithm": algorithm, "options": options} + srf_options = { + "index": mask_index, + "name": "Scalp", + "quality": _("Optimal *"), + "fill": True, + "keep_largest": False, + "overwrite": False, + } + Publisher.sendMessage( + "Create surface from index", + 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") + + 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] + 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() + + 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, auto_segment=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): @@ -428,10 +979,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 +1114,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: