Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Streamlined head surface creation #877

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e38230f
ADD: New page in the coregistration panel for easier head surface cre…
henrikkauppi Jan 15, 2025
2ba15ca
ADD: Button in coregistration to create head surface from mask and th…
henrikkauppi Jan 15, 2025
a1b1827
ADD: Streamlined head surface creation also removes non-visible faces…
henrikkauppi Jan 16, 2025
2c227a6
ADD: Bypass plugin GUI when removing non-visible faces in streamlined…
henrikkauppi Jan 17, 2025
1684397
MOD: Rename 'Head' page in coregistration panel as 'Surface' to be mo…
henrikkauppi Jan 17, 2025
eb365d0
Merge branch 'invesalius:master' into streamlined-head-surface-creation
henrikkauppi Jan 29, 2025
160981e
ADD: checkboxes to control the steps of the streamlined head surface …
henrikkauppi Jan 29, 2025
e354370
ADD: Brain segmentation step to the streamlined head surface creation
henrikkauppi Feb 6, 2025
82ba9b2
Merge branch 'invesalius:master' into streamlined-head-surface-creation
henrikkauppi Feb 6, 2025
8d11ed9
MOD: Show the navigation panel by default when opening InVesalius in …
henrikkauppi Feb 14, 2025
6799dd8
MOD: Set default colors and transparency for the created scalp and br…
henrikkauppi Feb 14, 2025
d69b7a7
MOD: Automate the brain segmentation process with default parameters
henrikkauppi Feb 14, 2025
dbaf95a
Merge branch 'invesalius:master' into streamlined-head-surface-creation
henrikkauppi Feb 25, 2025
6f3bd39
MOD: Integrate 'remove non-visible faces'-plugin into InVesalius and …
henrikkauppi Feb 25, 2025
79e42da
MOD: name created surfaces as scalp or brain
henrikkauppi Feb 25, 2025
4153030
MOD: keep the largest surface when creating a brain surface
henrikkauppi Feb 25, 2025
4eebbe3
ADD: Imports page to the coregistration panel for easier image and pr…
henrikkauppi Feb 28, 2025
70224a2
ADD: smooth surface and FIX: small bugs
rmatsuda Mar 4, 2025
5883d84
RUFF
rmatsuda Mar 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions invesalius/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
122 changes: 117 additions & 5 deletions invesalius/data/polydata_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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()
51 changes: 49 additions & 2 deletions invesalius/data/surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
25 changes: 21 additions & 4 deletions invesalius/gui/deep_learning_seg_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -393,14 +409,15 @@ def ShowProgress(self):


class BrainSegmenterDialog(DeepLearningSegmenterDialog):
def __init__(self, parent):
def __init__(self, parent, auto_segment=False):
super().__init__(
parent=parent,
title=_("Brain segmentation"),
has_torch=True,
has_plaidml=True,
has_theano=True,
segmenter=segment.BrainSegmentProcess,
auto_segment=auto_segment,
)


Expand Down
14 changes: 11 additions & 3 deletions invesalius/gui/default_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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")
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading