diff --git a/invesalius/data/brainmesh_handler.py b/invesalius/data/brainmesh_handler.py index 172b3c3..8441765 100644 --- a/invesalius/data/brainmesh_handler.py +++ b/invesalius/data/brainmesh_handler.py @@ -2,37 +2,95 @@ import vtk import pyacvd # import os import pyvista -# import numpy as np +import numpy as np # import Trekker +import invesalius.data.slice_ as sl +from invesalius.data.converters import to_vtk + class Brain: - def __init__(self, img_path, mask_path, n_peels, affine_vtk): + def __init__(self, n_peels, window_width, window_level, affine_vtk=None): # Create arrays to access the peel data and peel Actors self.peel = [] self.peelActors = [] - # Read the image - T1_reader = vtk.vtkNIFTIImageReader() - T1_reader.SetFileName(img_path) - T1_reader.Update() + self.window_width = window_width + self.window_level = window_level + self.numberOfPeels = n_peels + self.affine_vtk = affine_vtk + + def from_mask(self, mask): + mask= np.array(mask.matrix[1:, 1:, 1:]) + slic = sl.Slice() + image = slic.matrix + + mask = to_vtk(mask, spacing=slic.spacing) + image = to_vtk(image, spacing=slic.spacing) + + flip = vtk.vtkImageFlip() + flip.SetInputData(image) + flip.SetFilteredAxis(1) + flip.FlipAboutOriginOn() + flip.ReleaseDataFlagOn() + flip.Update() + image = flip.GetOutput() + + flip = vtk.vtkImageFlip() + flip.SetInputData(mask) + flip.SetFilteredAxis(1) + flip.FlipAboutOriginOn() + flip.ReleaseDataFlagOn() + flip.Update() + mask = flip.GetOutput() + # Image - self.refImage = T1_reader.GetOutput() + self.refImage = image + + self._do_surface_creation(mask) + + + def from_mask_file(self, mask_path): + slic = sl.Slice() + image = slic.matrix + image = to_vtk(image, spacing=slic.spacing) + # Read the mask mask_reader = vtk.vtkNIFTIImageReader() mask_reader.SetFileName(mask_path) mask_reader.Update() + + mask = mask_reader.GetOutput() + + mask_sFormMatrix = mask_reader.GetSFormMatrix() + + # Image + self.refImage = image + + self._do_surface_creation(mask, mask_sFormMatrix) + + + def _do_surface_creation(self, mask, mask_sFormMatrix=None, qFormMatrix=None): + if mask_sFormMatrix is None: + mask_sFormMatrix = vtk.vtkMatrix4x4() + + if qFormMatrix is None: + qFormMatrix = vtk.vtkMatrix4x4() + + value = np.mean(mask.GetScalarRange()) + # Use the mask to create isosurface mc = vtk.vtkContourFilter() - mc.SetInputConnection(mask_reader.GetOutputPort()) - mc.SetValue(0, 1) + mc.SetInputData(mask) + mc.SetValue(0, value) + mc.ComputeNormalsOn() mc.Update() # Mask isosurface refSurface = mc.GetOutput() + # Create a uniformly meshed surface tmpPeel = downsample(refSurface) # Standard space coordinates - mask_sFormMatrix = mask_reader.GetSFormMatrix() # Apply coordinate transform to the meshed mask mask_ijk2xyz = vtk.vtkTransform() @@ -48,7 +106,7 @@ class Brain: # Configure calculation of normals tmpPeel = fixMesh(tmpPeel) # Remove duplicate points etc - tmpPeel = cleanMesh(tmpPeel) + # tmpPeel = cleanMesh(tmpPeel) # Generate triangles tmpPeel = upsample(tmpPeel) @@ -56,9 +114,6 @@ class Brain: tmpPeel = fixMesh(tmpPeel) tmpPeel = cleanMesh(tmpPeel) - # Scanner coordinates from image - qFormMatrix = T1_reader.GetQFormMatrix() - refImageSpace2_xyz_transform = vtk.vtkTransform() refImageSpace2_xyz_transform.SetMatrix(qFormMatrix) @@ -83,16 +138,16 @@ class Brain: self.peel_centers = vtk.vtkFloatArray() self.peel.append(newPeel) self.currentPeelActor = vtk.vtkActor() - self.currentPeelActor.SetUserMatrix(affine_vtk) + if self.affine_vtk: + self.currentPeelActor.SetUserMatrix(self.affine_vtk) self.GetCurrentPeelActor(currentPeel) self.peelActors.append(self.currentPeelActor) # locator will later find the triangle on the peel surface where the coil's normal intersect self.locator = vtk.vtkCellLocator() - self.numberOfPeels = n_peels self.PeelDown(currentPeel) - def get_actor(self, n, affine_vtk): - return self.GetPeelActor(n, affine_vtk) + def get_actor(self, n): + return self.GetPeelActor(n) def SliceDown(self, currentPeel): # Warp using the normals @@ -159,9 +214,10 @@ class Brain: self.currentPeelNo += 1 - def TransformPeelPosition(self, p, affine_vtk): + def TransformPeelPosition(self, p): peel_transform = vtk.vtkTransform() - peel_transform.SetMatrix(affine_vtk) + if self.affine_vtk: + peel_transform.SetMatrix(self.affine_vtk) refpeelspace = vtk.vtkTransformPolyDataFilter() refpeelspace.SetInputData(self.peel[p]) refpeelspace.SetTransform(peel_transform) @@ -169,34 +225,26 @@ class Brain: currentPeel = refpeelspace.GetOutput() return currentPeel - def GetPeelActor(self, p, affine_vtk): - colors = vtk.vtkNamedColors() - # Create the color map - colorLookupTable = vtk.vtkLookupTable() - colorLookupTable.SetNumberOfColors(512) - colorLookupTable.SetSaturationRange(0, 0) - colorLookupTable.SetHueRange(0, 0) - colorLookupTable.SetValueRange(0, 1) - # colorLookupTable.SetTableRange(0, 1000) - # colorLookupTable.SetTableRange(0, 250) - colorLookupTable.SetTableRange(0, 200) - # colorLookupTable.SetTableRange(0, 150) - colorLookupTable.Build() + def GetPeelActor(self, p): + lut = vtk.vtkWindowLevelLookupTable() + lut.SetWindow(self.window_width) + lut.SetLevel(self.window_level) + lut.Build() + + init = self.window_level - self.window_width / 2 + end = self.window_level + self.window_width / 2 # Set mapper auto - mapper = vtk.vtkOpenGLPolyDataMapper() + mapper = vtk.vtkPolyDataMapper() mapper.SetInputData(self.peel[p]) - # mapper.SetScalarRange(0, 1000) - # mapper.SetScalarRange(0, 250) - mapper.SetScalarRange(0, 200) - # mapper.SetScalarRange(0, 150) - mapper.SetLookupTable(colorLookupTable) + mapper.SetScalarRange(init, end) + mapper.SetLookupTable(lut) mapper.InterpolateScalarsBeforeMappingOn() # Set actor self.currentPeelActor.SetMapper(mapper) - currentPeel = self.TransformPeelPosition(p, affine_vtk) + currentPeel = self.TransformPeelPosition(p) self.locator.SetDataSet(currentPeel) self.locator.BuildLocator() @@ -206,34 +254,26 @@ class Brain: return self.currentPeelActor def GetCurrentPeelActor(self, currentPeel): - colors = vtk.vtkNamedColors() - - # Create the color map - colorLookupTable = vtk.vtkLookupTable() - colorLookupTable.SetNumberOfColors(512) - colorLookupTable.SetSaturationRange(0, 0) - colorLookupTable.SetHueRange(0, 0) - colorLookupTable.SetValueRange(0, 1) - # colorLookupTable.SetTableRange(0, 1000) - # colorLookupTable.SetTableRange(0, 250) - colorLookupTable.SetTableRange(0, 200) - # colorLookupTable.SetTableRange(0, 150) - colorLookupTable.Build() + lut = vtk.vtkWindowLevelLookupTable() + lut.SetWindow(self.window_width) + lut.SetLevel(self.window_level) + lut.Build() + + init = self.window_level - self.window_width / 2 + end = self.window_level + self.window_width / 2 # Set mapper auto - mapper = vtk.vtkOpenGLPolyDataMapper() + mapper = vtk.vtkPolyDataMapper() mapper.SetInputData(currentPeel) - # mapper.SetScalarRange(0, 1000) - # mapper.SetScalarRange(0, 250) - mapper.SetScalarRange(0, 200) - # mapper.SetScalarRange(0, 150) - mapper.SetLookupTable(colorLookupTable) + mapper.SetScalarRange(init, end) + mapper.SetLookupTable(lut) mapper.InterpolateScalarsBeforeMappingOn() # Set actor self.currentPeelActor.SetMapper(mapper) self.currentPeelActor.GetProperty().SetBackfaceCulling(1) self.currentPeelActor.GetProperty().SetOpacity(0.5) + self.currentPeelActor.GetProperty().SetSpecular(0.25) return self.currentPeelActor diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index c6093c3..4511a71 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -46,6 +46,7 @@ except ImportError: from vtk.wx.wxVTKRenderWindowInteractor import wxVTKRenderWindowInteractor from wx.lib import masked from wx.lib.agw import floatspin +import wx.lib.filebrowsebutton as filebrowse from wx.lib.wordwrap import wordwrap from invesalius.pubsub import pub as Publisher import csv @@ -4995,3 +4996,134 @@ class SetSpacingDialog(wx.Dialog): def OnCancel(self, evt): self.EndModal(wx.ID_CANCEL) + + +class PeelsCreationDlg(wx.Dialog): + FROM_MASK = 1 + FROM_FILES = 2 + def __init__(self, parent, *args, **kwds): + wx.Dialog.__init__(self, parent, *args, **kwds) + + self.mask_path = '' + self.method = self.FROM_MASK + + self._init_gui() + self._bind_events_wx() + self.get_all_masks() + + def _init_gui(self): + self.SetTitle("dialog") + + from_mask_stbox = self._from_mask_gui() + from_files_stbox = self._from_files_gui() + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(from_mask_stbox, 0, wx.EXPAND | wx.ALL, 5) + main_sizer.Add(from_files_stbox, 0, wx.EXPAND | wx.ALL, 5) + + btn_sizer = wx.StdDialogButtonSizer() + main_sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4) + + self.btn_ok = wx.Button(self, wx.ID_OK, "") + self.btn_ok.SetDefault() + btn_sizer.AddButton(self.btn_ok) + + self.btn_cancel = wx.Button(self, wx.ID_CANCEL, "") + btn_sizer.AddButton(self.btn_cancel) + + btn_sizer.Realize() + + self.SetSizer(main_sizer) + main_sizer.Fit(self) + + self.SetAffirmativeId(self.btn_ok.GetId()) + self.SetEscapeId(self.btn_cancel.GetId()) + + self.Layout() + + def _from_mask_gui(self): + mask_box = wx.StaticBox(self, -1, _("From mask")) + from_mask_stbox = wx.StaticBoxSizer(mask_box, wx.VERTICAL) + + self.cb_masks = wx.ComboBox(self, wx.ID_ANY, choices=[]) + self.from_mask_rb = wx.RadioButton(self, -1, "", style = wx.RB_GROUP) + + internal_sizer = wx.BoxSizer(wx.HORIZONTAL) + internal_sizer.Add(self.from_mask_rb, 0, wx.ALL | wx.EXPAND, 5) + internal_sizer.Add(self.cb_masks, 1, wx.ALL | wx.EXPAND, 5) + + from_mask_stbox.Add(internal_sizer, 0, wx.EXPAND) + + return from_mask_stbox + + def _from_files_gui(self): + session = ses.Session() + last_directory = session.get('paths', 'last_directory_%d' % const.ID_NIFTI_IMPORT, '') + + files_box = wx.StaticBox(self, -1, _("From files")) + from_files_stbox = wx.StaticBoxSizer(files_box, wx.VERTICAL) + + self.mask_file_browse = filebrowse.FileBrowseButton(self, -1, labelText=_("Mask file"), + fileMask=WILDCARD_NIFTI, dialogTitle=_("Choose Mask file"), startDirectory = last_directory, + changeCallback=lambda evt: self._set_files_callback(mask_path=evt.GetString())) + self.from_files_rb = wx.RadioButton(self, -1, "") + + ctrl_sizer = wx.BoxSizer(wx.VERTICAL) + ctrl_sizer.Add(self.mask_file_browse, 0, wx.ALL | wx.EXPAND, 5) + + internal_sizer = wx.BoxSizer(wx.HORIZONTAL) + internal_sizer.Add(self.from_files_rb, 0, wx.ALL | wx.EXPAND, 5) + internal_sizer.Add(ctrl_sizer, 0, wx.ALL | wx.EXPAND, 5) + + from_files_stbox.Add(internal_sizer, 0, wx.EXPAND) + + return from_files_stbox + + def _bind_events_wx(self): + self.from_mask_rb.Bind(wx.EVT_RADIOBUTTON, self.on_select_method) + self.from_files_rb.Bind(wx.EVT_RADIOBUTTON, self.on_select_method) + + def get_all_masks(self): + import invesalius.project as prj + inv_proj = prj.Project() + choices = [i.name for i in inv_proj.mask_dict.values()] + try: + initial_value = choices[0] + enable = True + except IndexError: + initial_value = "" + enable = False + + self.cb_masks.SetItems(choices) + self.cb_masks.SetValue(initial_value) + self.btn_ok.Enable(enable) + + def on_select_method(self, evt): + radio_selected = evt.GetEventObject() + if radio_selected is self.from_mask_rb: + self.method = self.FROM_MASK + if self.cb_masks.GetItems(): + self.btn_ok.Enable(True) + else: + self.btn_ok.Enable(False) + else: + self.method = self.FROM_FILES + if self._check_if_files_exists(): + self.btn_ok.Enable(True) + else: + self.btn_ok.Enable(False) + + def _set_files_callback(self, mask_path=''): + if mask_path: + self.mask_path = mask_path + if self.method == self.FROM_FILES: + if self._check_if_files_exists(): + self.btn_ok.Enable(True) + else: + self.btn_ok.Enable(False) + + def _check_if_files_exists(self): + if self.mask_path and os.path.exists(self.mask_path): + return True + else: + return False diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index e7691a7..04b3c2e 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -31,7 +31,8 @@ try: import Trekker has_trekker = True except ImportError: - has_trekker = False + has_trekker = True + try: import invesalius.data.elfin as elfin import invesalius.data.elfin_processing as elfin_process @@ -40,6 +41,7 @@ except ImportError: has_robot = False import wx +import vtk try: import wx.lib.agw.foldpanelbar as fpb @@ -72,6 +74,7 @@ from invesalius.navigation.icp import ICP from invesalius.navigation.navigation import Navigation from invesalius.navigation.tracker import Tracker from invesalius.navigation.robot import Robot +from invesalius.data.converters import to_vtk from invesalius.net.neuronavigation_api import NeuronavigationApi @@ -1939,7 +1942,7 @@ class TractographyPanel(wx.Panel): def OnSelectPeelingDepth(self, evt, ctrl): self.peel_depth = ctrl.GetValue() if self.checkpeeling.GetValue(): - actor = self.brain_peel.get_actor(self.peel_depth, self.affine_vtk) + actor = self.brain_peel.get_actor(self.peel_depth) Publisher.sendMessage('Update peel', flag=True, actor=actor) Publisher.sendMessage('Get peel centers and normals', centers=self.brain_peel.peel_centers, normals=self.brain_peel.peel_normals) @@ -1972,7 +1975,7 @@ class TractographyPanel(wx.Panel): def OnShowPeeling(self, evt, ctrl): # self.view_peeling = ctrl.GetValue() if ctrl.GetValue(): - actor = self.brain_peel.get_actor(self.peel_depth, self.affine_vtk) + actor = self.brain_peel.get_actor(self.peel_depth) self.peel_loaded = True Publisher.sendMessage('Update peel visualization', data=self.peel_loaded) else: @@ -2002,46 +2005,48 @@ class TractographyPanel(wx.Panel): def OnLinkBrain(self, event=None): Publisher.sendMessage('Begin busy cursor') - mask_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import brain mask")) - img_path = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, _("Import T1 anatomical image")) - # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609' - # mask_file = 'Baran_brain_mask.nii' - # mask_path = os.path.join(data_dir, mask_file) - # img_file = 'Baran_T1_inFODspace.nii' - # img_path = os.path.join(data_dir, img_file) - - if not self.affine_vtk: + inv_proj = prj.Project() + peels_dlg = dlg.PeelsCreationDlg(wx.GetApp().GetTopWindow()) + ret = peels_dlg.ShowModal() + method = peels_dlg.method + if ret == wx.ID_OK: slic = sl.Slice() - prj_data = prj.Project() - matrix_shape = tuple(prj_data.matrix_shape) - spacing = tuple(prj_data.spacing) - img_shift = spacing[1] * (matrix_shape[1] - 1) - self.affine = slic.affine.copy() - self.affine[1, -1] -= img_shift - self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine) - - if mask_path and img_path: - Publisher.sendMessage('Update status text in GUI', label=_("Busy")) - try: - self.brain_peel = brain.Brain(img_path, mask_path, self.n_peels, self.affine_vtk) - self.brain_actor = self.brain_peel.get_actor(self.peel_depth, self.affine_vtk) - self.brain_actor.GetProperty().SetOpacity(self.brain_opacity) - - self.checkpeeling.Enable(1) - self.checkpeeling.SetValue(True) - self.spin_opacity.Enable(1) - self.peel_loaded = True - - Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor) - Publisher.sendMessage('Get peel centers and normals', centers=self.brain_peel.peel_centers, - normals=self.brain_peel.peel_normals) - Publisher.sendMessage('Get init locator', locator=self.brain_peel.locator) - Publisher.sendMessage('Update status text in GUI', label=_("Brain model loaded")) - Publisher.sendMessage('Update peel visualization', data= self.peel_loaded) - except: - Publisher.sendMessage('Update status text in GUI', label=_("Brain mask initialization failed.")) - wx.MessageBox(_("Unable to load brain mask."), _("InVesalius 3")) + ww = slic.window_width + wl = slic.window_level + affine_vtk = vtk.vtkMatrix4x4() + + if method == peels_dlg.FROM_FILES: + matrix_shape = tuple(inv_proj.matrix_shape) + try: + affine = slic.affine.copy() + except AttributeError: + affine = np.eye(4) + affine[1, -1] -= matrix_shape[1] + affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) + + self.brain_peel = brain.Brain(self.n_peels, ww, wl, affine_vtk) + if method == peels_dlg.FROM_MASK: + choices = [i for i in inv_proj.mask_dict.values()] + mask_index = peels_dlg.cb_masks.GetSelection() + mask = choices[mask_index] + self.brain_peel.from_mask(mask) + else: + mask_path = peels_dlg.mask_path + self.brain_peel.from_mask_file(mask_path) + self.brain_actor = self.brain_peel.get_actor(self.peel_depth) + self.brain_actor.GetProperty().SetOpacity(self.brain_opacity) + Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor) + Publisher.sendMessage('Get peel centers and normals', centers=self.brain_peel.peel_centers, + normals=self.brain_peel.peel_normals) + Publisher.sendMessage('Get init locator', locator=self.brain_peel.locator) + self.checkpeeling.Enable(1) + self.checkpeeling.SetValue(True) + self.spin_opacity.Enable(1) + Publisher.sendMessage('Update status text in GUI', label=_("Brain model loaded")) + self.peel_loaded = True + Publisher.sendMessage('Update peel visualization', data= self.peel_loaded) + peels_dlg.Destroy() Publisher.sendMessage('End busy cursor') def OnLinkFOD(self, event=None): -- libgit2 0.21.2