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

Added Mask 3D edit #822

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
channels:
- conda-forge
- bioconda
- pytorch
dependencies:
- python==3.11
- cython==3.0.10
Expand Down
10 changes: 10 additions & 0 deletions invesalius/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@
ID_MASK_3D_PREVIEW = wx.NewIdRef()
ID_MASK_3D_RELOAD = wx.NewIdRef()
ID_MASK_3D_AUTO_RELOAD = wx.NewIdRef()
ID_MASK_3D_EDIT = wx.NewIdRef()

ID_GOTO_SLICE = wx.NewIdRef()
ID_GOTO_COORD = wx.NewIdRef()
Expand Down Expand Up @@ -648,6 +649,7 @@
STATE_MEASURE_DENSITY_POLYGON = 1011
STATE_NAVIGATION = 1012
STATE_REGISTRATION = 1013
STATE_MASK_3D_EDIT = 1014

SLICE_STATE_CROSS = 3006
SLICE_STATE_SCROLL = 3007
Expand Down Expand Up @@ -727,6 +729,8 @@
STATE_REGISTRATION: 3,
# Override all other states when in navigation mode.
STATE_NAVIGATION: 4,
# TODO(Henrique): check whether we need to override the previous states
STATE_MASK_3D_EDIT: 3,
}

# ------------ Prefereces options key ------------
Expand Down Expand Up @@ -1050,3 +1054,9 @@
# Pedal
KEYSTROKE_PEDAL_ENABLED = True
KEYSTROKE_PEDAL_KEY = wx.WXK_F21

# Mask 3D Edit modes

MASK_3D_EDIT_INCLUDE = 0
MASK_3D_EDIT_EXCLUDE = 1
MASK_3D_EDIT_OP_NAME = [_("Include Inside"), _("Exclude Inside")]
71 changes: 71 additions & 0 deletions invesalius/data/polygon_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# --------------------------------------------------------------------------
# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
# Homepage: http://www.softwarepublico.gov.br
# Contact: [email protected]
# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
# --------------------------------------------------------------------------
# Este programa e software livre; voce pode redistribui-lo e/ou
# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
# publicada pela Free Software Foundation; de acordo com a versao 2
# da Licenca.
#
# Este programa eh distribuido na expectativa de ser util, mas SEM
# QUALQUER GARANTIA; sem mesmo a garantia implicita de
# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
# detalhes.
# --------------------------------------------------------------------------

import invesalius.constants as const
from invesalius.gui.widgets.canvas_renderer import CanvasHandlerBase, Polygon

# import invesalius.session as ses


class PolygonSelectCanvas(CanvasHandlerBase):
"""
Inspired on PolygonDensityMeasure, stores and renders a polygon for a canvas
"""

def __init__(self, colour=(255, 0, 0, 255), interactive=True):
super(PolygonSelectCanvas, self).__init__(None)
self.parent = None
self.children = []
self.layer = 0

self.colour = colour
self.points = []

self.complete = False

self.location = const.SURFACE
self.index = 0

self.polygon = Polygon(self, fill=False, closed=False, line_colour=self.colour, is_3d=False)
self.polygon.layer = 1
self.add_child(self.polygon)

self.text_box = None
self.interactive = interactive

def draw_to_canvas(self, gc, canvas):
pass

def insert_point(self, point):
self.polygon.append_point(point)

def complete_polygon(self):
if len(self.polygon.points) >= 3:
self.polygon.closed = True
self.complete = True

def IsComplete(self):
return self.complete

def SetVisibility(self, value):
self.polygon.visible = value

def set_interactive(self, value):
self.interactive = bool(value)
self.polygon.interactive = self.interactive
103 changes: 103 additions & 0 deletions invesalius/data/styles_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,24 @@

import invesalius.constants as const
import invesalius.project as prj
import invesalius.segmentation.mask_3d_edit as m3e
from invesalius.data.polygon_select import PolygonSelectCanvas
from invesalius.pubsub import pub as Publisher

PROP_MEASURE = 0.8

import numpy as np


# TODO: find a better place
def vtkarray_to_numpy(m):
nm = np.zeros((4, 4))
for i in range(4):
for j in range(4):
nm[i, j] = m.GetElement(i, j)

return nm


class Base3DInteractorStyle(vtkInteractorStyleTrackballCamera):
def __init__(self, viewer):
Expand Down Expand Up @@ -620,6 +634,94 @@ def OnScrollBackward(self, evt, obj):
super().OnScrollBackward(evt, obj)


class Mask3DEditorInteractorStyle(DefaultInteractorStyle):
"""
Interactor style for selecting a polygon of interest and performing a mesh edit based on that.
"""

def __init__(self, viewer):
super().__init__(viewer)
self.viewer = viewer

# Manages the mask operations
self.mask3deditor = m3e.Mask3DEditor()

self.picker = vtkCellPicker()
self.picker.PickFromListOn()

self.has_open_poly = False
self.poly = None

self.RemoveObservers("LeftButtonPressEvent")
self.AddObserver("LeftButtonPressEvent", self.OnInsertPolygonPoint)
self.AddObserver("RightButtonPressEvent", self.OnInsertPolygon)

Publisher.subscribe(self.ClearPolygons, "M3E clear polygons")

def CleanUp(self):
self.RemoveObservers("LeftButtonPressEvent")
self.RemoveObservers("RightButtonPressEvent")

def ClearPolygons(self):
self.viewer.canvas.draw_list.clear()
self.viewer.UpdateCanvas()

def OnInsertPolygonPoint(self, obj, evt):
interactor = self.viewer.interactor
interactor.Render()

mouse_x, mouse_y = self.viewer.interactor.GetEventPosition()

if not self.has_open_poly:
self.poly = PolygonSelectCanvas()
self.has_open_poly = True
self.viewer.canvas.draw_list.append(self.poly)

self.poly.insert_point((mouse_x, mouse_y))
self.viewer.UpdateCanvas()

def __get_cam_parameters(self):
w, h = self.viewer.GetSize()
self.viewer.ren.Render()
cam = self.viewer.ren.GetActiveCamera()
near, far = cam.GetClippingRange()

# This flip around the Y axis was done to countereffect the flip that vtk performs
# in volume.py:780. If we do not flip back, what is being displayed is flipped,
# although the actual coordinates are the initial ones, so the cutting gets wrong
# after rotations around y or x.
inv_Y_matrix = np.eye(4)
inv_Y_matrix[1, 1] = -1

# Composite transform world to viewport (projection * view)
M = cam.GetCompositeProjectionTransformMatrix(w / float(h), near, far)
M = vtkarray_to_numpy(M)
M = M @ inv_Y_matrix

MV = cam.GetViewTransformMatrix()
MV = vtkarray_to_numpy(MV)
MV = MV @ inv_Y_matrix

params = {
"model_to_screen": M,
"model_view": MV,
"resolution": (w, h),
"clipping_range": (near, far),
}

return params

def OnInsertPolygon(self, obj, evt):
self.poly.complete_polygon()
self.has_open_poly = False

self.viewer.UpdateCanvas()
self.viewer.ren.Render()

Publisher.sendMessage("M3E add polygon", points=self.poly.polygon.points)
Publisher.sendMessage("M3E set camera", params=self.__get_cam_parameters())


class Styles:
styles = {
const.STATE_DEFAULT: DefaultInteractorStyle,
Expand All @@ -634,6 +736,7 @@ class Styles:
const.SLICE_STATE_CROSS: CrossInteractorStyle,
const.STATE_NAVIGATION: NavigationInteractorStyle,
const.STATE_REGISTRATION: RegistrationInteractorStyle,
const.STATE_MASK_3D_EDIT: Mask3DEditorInteractorStyle,
}

@classmethod
Expand Down
76 changes: 76 additions & 0 deletions invesalius/gui/task_slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,42 @@ def __init__(self, parent):
self.gradient_thresh = gradient_thresh
self.bind_evt_gradient = True

## LINE 5
m3ediv = wx.StaticLine(
self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL
)

cbox_mask_edit_3d = wx.CheckBox(self, -1, "Edit in 3D")
btn_do_3d_edit = wx.Button(self, -1, _("Cut"))
combo_mask_edit_3d_op = wx.ComboBox(
self, -1, "", choices=const.MASK_3D_EDIT_OP_NAME, style=wx.CB_DROPDOWN | wx.CB_READONLY
)
combo_mask_edit_3d_op.SetSelection(const.MASK_3D_EDIT_INCLUDE)

line5 = wx.BoxSizer(wx.HORIZONTAL)
# line5.Add(txt_mesh_edit_3d)
line5.Add(cbox_mask_edit_3d)
line5.Add(btn_do_3d_edit)
line5.Add(combo_mask_edit_3d_op)

self.cbox_mask_edit_3d = cbox_mask_edit_3d
self.btn_do_3d_edit = btn_do_3d_edit
self.combo_mask_edit_3d_op = combo_mask_edit_3d_op

## LINE 6
cbox_mask_edit_3d_depth = wx.CheckBox(self, -1, "Cut with depth")
spin_mask_edit_3d_depth = wx.SpinCtrlDouble(self, value="1.0", min=0.0, max=1.0, inc=0.05)
btn_clear_3d_poly = wx.Button(self, -1, _("Clear Polygons"))

line6 = wx.BoxSizer(wx.HORIZONTAL)
line6.Add(cbox_mask_edit_3d_depth)
line6.Add(spin_mask_edit_3d_depth)
line6.Add(btn_clear_3d_poly)

self.cbox_mask_edit_3d_depth = cbox_mask_edit_3d_depth
self.spin_mask_edit_3d_depth = spin_mask_edit_3d_depth
self.btn_clear_3d_poly = btn_clear_3d_poly

# Add lines into main sizer
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.AddSpacer(7)
Expand All @@ -768,6 +804,12 @@ def __init__(self, parent):
sizer.Add(text_thresh, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
sizer.AddSpacer(5)
sizer.Add(gradient_thresh, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
sizer.AddSpacer(5)
sizer.Add(m3ediv, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
sizer.AddSpacer(3)
sizer.Add(line5, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
sizer.AddSpacer(3)
sizer.Add(line6, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
sizer.AddSpacer(7)
sizer.Fit(self)

Expand All @@ -782,6 +824,12 @@ def __bind_events_wx(self):
self.btn_brush_format.Bind(wx.EVT_MENU, self.OnMenu)
self.Bind(grad.EVT_THRESHOLD_CHANGED, self.OnGradientChanged, self.gradient_thresh)
self.combo_brush_op.Bind(wx.EVT_COMBOBOX, self.OnComboBrushOp)
self.cbox_mask_edit_3d.Bind(wx.EVT_CHECKBOX, self.OnCheckMaskEdit3D)
self.btn_do_3d_edit.Bind(wx.EVT_BUTTON, self.OnDoMaskEdit3D)
self.combo_mask_edit_3d_op.Bind(wx.EVT_COMBOBOX, self.OnComboMaskEdit3DMode)
self.cbox_mask_edit_3d_depth.Bind(wx.EVT_CHECKBOX, self.OnUseDepthMaskEdit3D)
self.spin_mask_edit_3d_depth.Bind(wx.EVT_SPINCTRLDOUBLE, self.OnSpinDepthMaskEdit3D)
self.btn_clear_3d_poly.Bind(wx.EVT_BUTTON, self.OnClearPolyMaskEdit3D)

def __bind_events(self):
Publisher.subscribe(self.SetThresholdBounds, "Update threshold limits")
Expand Down Expand Up @@ -885,6 +933,34 @@ def OnSetUnit(self, evt):
self.unit = self.txt_unit.GetLabel()
Publisher.sendMessage("Set edition brush unit", unit=self.unit)

def OnCheckMaskEdit3D(self, evt):
style_id = const.STATE_MASK_3D_EDIT

if self.cbox_mask_edit_3d.GetValue():
Publisher.sendMessage("Enable style", style=style_id)
Publisher.sendMessage("Enable mask 3D preview")
else:
Publisher.sendMessage("Disable style", style=style_id)

def OnDoMaskEdit3D(self, evt):
Publisher.sendMessage("M3E apply edit")

def OnComboMaskEdit3DMode(self, evt):
op_id = evt.GetSelection()
Publisher.sendMessage("M3E set edit mode", mode=op_id)

def OnUseDepthMaskEdit3D(self, evt):
cb_val = self.cbox_mask_edit_3d_depth.GetValue()
Publisher.sendMessage("M3E use depth", use=cb_val)
self.OnSpinDepthMaskEdit3D(evt) # To pass the set value

def OnSpinDepthMaskEdit3D(self, evt):
spin_val = self.spin_mask_edit_3d_depth.GetValue()
Publisher.sendMessage("M3E depth value", value=spin_val)

def OnClearPolyMaskEdit3D(self, evt):
Publisher.sendMessage("M3E clear polygons")


class WatershedTool(EditionTools):
def __init__(self, parent):
Expand Down
Loading