From 99e6d36b1697f260027dee9cfac67603dffdfdce Mon Sep 17 00:00:00 2001 From: Thiago Franco de Moraes Date: Tue, 26 Jan 2021 17:32:36 -0300 Subject: [PATCH] Mask 3d preview (#252) --- invesalius/constants.py | 3 +++ invesalius/control.py | 33 +++++++++++++++++++++++++++++++++ invesalius/data/converters.py | 34 ++++++++++++++++++++++++++++++++-- invesalius/data/mask.py | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ invesalius/data/slice_.py | 21 +++++++++++++++++++-- invesalius/data/styles.py | 11 ++++++----- invesalius/data/viewer_volume.py | 20 +++++++++++++++++--- invesalius/data/volume.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- invesalius/gui/data_notebook.py | 7 ++----- invesalius/gui/default_viewers.py | 71 +++++++++++++++++++++++------------------------------------------------ invesalius/gui/frame.py | 42 ++++++++++++++++++++++++++++++++++++++++++ invesalius/project.py | 3 +++ invesalius/segmentation/brain/segment.py | 2 +- invesalius/session.py | 8 ++++++-- invesalius/utils.py | 11 +++++++++++ 15 files changed, 385 insertions(+), 83 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index fd0d20b..9c45c45 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -516,6 +516,9 @@ ID_DENSITY_MEASURE = wx.NewId() ID_MASK_DENSITY_MEASURE = wx.NewId() ID_CREATE_SURFACE = wx.NewId() ID_CREATE_MASK = wx.NewId() +ID_MASK_3D_PREVIEW = wx.NewId() +ID_MASK_3D_RELOAD = wx.NewId() +ID_MASK_3D_AUTO_RELOAD = wx.NewId() ID_GOTO_SLICE = wx.NewId() ID_GOTO_COORD = wx.NewId() diff --git a/invesalius/control.py b/invesalius/control.py index 001cb5b..72b0f9e 100644 --- a/invesalius/control.py +++ b/invesalius/control.py @@ -126,6 +126,12 @@ class Controller(): Publisher.subscribe(self.create_project_from_matrix, 'Create project from matrix') + Publisher.subscribe(self.show_mask_preview, 'Show mask preview') + + Publisher.subscribe(self.enable_mask_preview, 'Enable mask 3D preview') + Publisher.subscribe(self.disable_mask_preview, 'Disable mask 3D preview') + Publisher.subscribe(self.update_mask_preview, 'Update mask 3D preview') + def SetBitmapSpacing(self, spacing): proj = prj.Project() proj.spacing = spacing @@ -1108,3 +1114,30 @@ class Controller(): if err_msg: dialog.MessageBox(None, "It was not possible to launch new instance of InVesalius3 dsfa dfdsfa sdfas fdsaf asdfasf dsaa", err_msg) + + def show_mask_preview(self, index, flag=True): + proj = prj.Project() + mask = proj.mask_dict[index] + slc = self.Slice.do_threshold_to_all_slices(mask) + mask.create_3d_preview() + Publisher.sendMessage("Load mask preview", mask_3d_actor=mask.volume._actor, flag=flag) + Publisher.sendMessage("Reload actual slice") + + def enable_mask_preview(self): + mask = self.Slice.current_mask + if mask is not None: + self.Slice.do_threshold_to_all_slices(mask) + mask.create_3d_preview() + Publisher.sendMessage("Load mask preview", mask_3d_actor=mask.volume._actor, flag=True) + Publisher.sendMessage("Render volume viewer") + + def disable_mask_preview(self): + mask = self.Slice.current_mask + if mask is not None: + Publisher.sendMessage("Remove mask preview", mask_3d_actor=mask.volume._actor) + Publisher.sendMessage("Render volume viewer") + + def update_mask_preview(self): + mask = self.Slice.current_mask + if mask is not None: + mask._update_imagedata() diff --git a/invesalius/data/converters.py b/invesalius/data/converters.py index 418b4f2..dcc9ed3 100644 --- a/invesalius/data/converters.py +++ b/invesalius/data/converters.py @@ -24,8 +24,7 @@ import vtk from vtk.util import numpy_support -def to_vtk(n_array, spacing, slice_number, orientation, origin=(0, 0, 0), padding=(0, 0, 0)): - +def to_vtk(n_array, spacing=(1.0, 1.0, 1.0), slice_number=0, orientation='AXIAL', origin=(0, 0, 0), padding=(0, 0, 0)): if orientation == "SAGITTAL": orientation = "SAGITAL" @@ -67,6 +66,37 @@ def to_vtk(n_array, spacing, slice_number, orientation, origin=(0, 0, 0), paddin return image_copy +def to_vtk_mask(n_array, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0)): + dz, dy, dx = n_array.shape + ox, oy, oz = origin + sx, sy, sz = spacing + + ox -= sx + oy -= sy + oz -= sz + + v_image = numpy_support.numpy_to_vtk(n_array.flat) + extent = (0, dx - 1, 0, dy - 1, 0, dz - 1) + + # Generating the vtkImageData + image = vtk.vtkImageData() + image.SetOrigin(ox, oy, oz) + image.SetSpacing(sx, sy, sz) + image.SetDimensions(dx - 1, dy - 1, dz - 1) + # SetNumberOfScalarComponents and SetScalrType were replaced by + # AllocateScalars + # image.SetNumberOfScalarComponents(1) + # image.SetScalarType(numpy_support.get_vtk_array_type(n_array.dtype)) + image.AllocateScalars(numpy_support.get_vtk_array_type(n_array.dtype), 1) + image.SetExtent(extent) + image.GetPointData().SetScalars(v_image) + + # image_copy = vtk.vtkImageData() + # image_copy.DeepCopy(image) + + return image + + def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)): dy, dx, dc = n_array.shape v_image = numpy_support.numpy_to_vtk(n_array.reshape(dy*dx, dc)) diff --git a/invesalius/data/mask.py b/invesalius/data/mask.py index 6ee6f38..ec4c1a8 100644 --- a/invesalius/data/mask.py +++ b/invesalius/data/mask.py @@ -22,18 +22,21 @@ import plistlib import random import shutil import tempfile - -import numpy as np -import vtk +import time +import weakref import invesalius.constants as const +import invesalius.data.converters as converters import invesalius.data.imagedata_utils as iu import invesalius.session as ses - +from invesalius.data.volume import VolumeMask +import numpy as np +import vtk from invesalius_cy import floodfill - from pubsub import pub as Publisher from scipy import ndimage +from vtk.util import numpy_support + class EditionHistoryNode(object): def __init__(self, index, orientation, array, clean=False): @@ -189,7 +192,9 @@ class Mask(): def __init__(self): Mask.general_index += 1 self.index = Mask.general_index - self.imagedata = '' + self.matrix = None + self.spacing = (1.0, 1.0, 1.0) + self.imagedata = None self.colour = random.choice(const.MASK_COLOUR) self.opacity = const.MASK_OPACITY self.threshold_range = const.THRESHOLD_RANGE @@ -198,7 +203,11 @@ class Mask(): self.is_shown = 1 self.edited_points = {} self.was_edited = False + self.volume = None + self.auto_update_mask = True + self.modified_time = 0 self.__bind_events() + self._modified_callbacks = [] self.history = EditionHistory() @@ -206,11 +215,24 @@ class Mask(): Publisher.subscribe(self.OnFlipVolume, 'Flip volume') Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') + def as_vtkimagedata(self): + print("Converting to VTK") + vimg = converters.to_vtk_mask(self.matrix, self.spacing) + print("Converted") + return vimg + + def set_colour(self, colour): + self.colour = colour + if self.volume is not None: + self.volume.set_colour(colour) + Publisher.sendMessage("Render volume viewer") + def save_history(self, index, orientation, array, p_array, clean=False): self.history.new_node(index, orientation, array, p_array, clean) def undo_history(self, actual_slices): self.history.undo(self.matrix, actual_slices) + self.modified() # Marking the project as changed session = ses.Session() @@ -218,6 +240,7 @@ class Mask(): def redo_history(self, actual_slices): self.history.redo(self.matrix, actual_slices) + self.modified() # Marking the project as changed session = ses.Session() @@ -226,6 +249,27 @@ class Mask(): def on_show(self): self.history._config_undo_redo(self.is_shown) + def create_3d_preview(self): + if self.volume is None: + if self.imagedata is None: + self.imagedata = self.as_vtkimagedata() + self.volume = VolumeMask(self) + self.volume.create_volume() + + def _update_imagedata(self, update_volume_viewer=True): + if self.imagedata is not None: + dz, dy, dx = self.matrix.shape + # np_image = numpy_support.vtk_to_numpy(self.imagedata.GetPointData().GetScalars()) + # np_image[:] = self.matrix.reshape(-1) + self.imagedata.SetDimensions(dx - 1, dy - 1, dz - 1) + self.imagedata.SetSpacing(self.spacing) + self.imagedata.SetExtent(0, dx - 1, 0, dy - 1, 0, dz - 1) + self.imagedata.Modified() + self.volume._actor.Update() + + if update_volume_viewer: + Publisher.sendMessage("Render volume viewer") + def SavePlist(self, dir_temp, filelist): mask = {} filename = u'mask_%d' % self.index @@ -284,10 +328,15 @@ class Mask(): elif axis == 2: submatrix[:] = submatrix[:, :, ::-1] self.matrix[0, 0, 1::] = self.matrix[0, 0, :0:-1] + self.modified() def OnSwapVolumeAxes(self, axes): axis0, axis1 = axes self.matrix = self.matrix.swapaxes(axis0, axis1) + if self.volume: + self.imagedata = self.as_vtkimagedata() + self.volume.change_imagedata() + self.modified() def _save_mask(self, filename): shutil.copyfile(self.temp_file, filename) @@ -300,6 +349,22 @@ class Mask(): def _set_class_index(self, index): Mask.general_index = index + def add_modified_callback(self, callback): + ref = weakref.WeakMethod(callback) + self._modified_callbacks.append(ref) + + def remove_modified_callback(self, callback): + callbacks = [] + removed = False + for cb in self._modified_callbacks: + if cb() is not None: + if cb() != callback: + callbacks.append(cb) + else: + removed = True + self._modified_callbacks = callbacks + return removed + def create_mask(self, shape): """ Creates a new mask object. This method do not append this new mask into the project. @@ -311,11 +376,35 @@ class Mask(): shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 self.matrix = np.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) + def modified(self, all_volume=False): + if all_volume: + self.matrix[0] = 1 + self.matrix[:, 0, :] = 1 + self.matrix[:, :, 0] = 1 + if ses.Session().auto_reload_preview: + self._update_imagedata() + self.modified_time = time.monotonic() + callbacks = [] + print(self._modified_callbacks) + for callback in self._modified_callbacks: + if callback() is not None: + callback()() + callbacks.append(callback) + self._modified_callbacks = callbacks + def clean(self): self.matrix[1:, 1:, 1:] = 0 - self.matrix[0, :, :] = 1 - self.matrix[:, 0, :] = 1 - self.matrix[:, :, 0] = 1 + self.modified(all_volume=True) + + def cleanup(self): + if self.is_shown: + self.history._config_undo_redo(False) + if self.volume: + Publisher.sendMessage("Unload volume", volume=self.volume._actor) + Publisher.sendMessage("Render volume viewer") + self.imagedata = None + self.volume = None + del self.matrix def copy(self, copy_name): """ @@ -334,6 +423,7 @@ class Mask(): new_mask.create_mask(shape=[i-1 for i in self.matrix.shape]) new_mask.matrix[:] = self.matrix[:] + new_mask.spacing = self.spacing return new_mask @@ -384,7 +474,4 @@ class Mask(): self.save_history(index, orientation, matrix.copy(), cp_mask) def __del__(self): - if self.is_shown: - self.history._config_undo_redo(False) - del self.matrix os.remove(self.temp_file) diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index ef62bf1..3461d32 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -384,9 +384,21 @@ class Slice(metaclass=utils.Singleton): self.current_mask.matrix[:] = 0 self.current_mask.clear_history() + if self.current_mask.auto_update_mask and self.current_mask.volume is not None: + to_reload = True + self.SetMaskThreshold( + index, + threshold_range, + slice_number = None, + orientation = None + ) + self.discard_all_buffers() + Publisher.sendMessage("Reload actual slice") + self.current_mask.modified(all_volume=True) + return + to_reload = False if threshold_range != self.current_mask.threshold_range: - to_reload = True for orientation in self.buffer_slices: self.buffer_slices[orientation].discard_vtk_mask() self.SetMaskThreshold( @@ -417,6 +429,7 @@ class Slice(metaclass=utils.Singleton): if to_reload: Publisher.sendMessage("Reload actual slice") + self.current_mask.modified(all_volume=False) def __set_current_mask_threshold_actual_slice(self, threshold_range): if self.current_mask is None: @@ -1061,7 +1074,7 @@ class Slice(metaclass=utils.Singleton): def SetMaskColour(self, index, colour, update=True): "Set a mask colour given its index and colour (RGB 0-1 values)" proj = Project() - proj.mask_dict[index].colour = colour + proj.mask_dict[index].set_colour(colour) (r, g, b) = colour[:3] colour_wx = [r * 255, g * 255, b * 255] @@ -1108,6 +1121,7 @@ class Slice(metaclass=utils.Singleton): # TODO: find out a better way to do threshold if slice_number is None: for n, slice_ in enumerate(self.matrix): + print(n) m = np.ones(slice_.shape, self.current_mask.matrix.dtype) m[slice_ < thresh_min] = 0 m[slice_ > thresh_max] = 0 @@ -1346,6 +1360,7 @@ class Slice(metaclass=utils.Singleton): """ future_mask = Mask() future_mask.create_mask(self.matrix.shape) + future_mask.spacing = self.spacing if name: future_mask.name = name @@ -1605,6 +1620,7 @@ class Slice(metaclass=utils.Singleton): future_mask = Mask() future_mask.create_mask(self.matrix.shape) + future_mask.spacing = spacing future_mask.name = new_name future_mask.matrix[:] = 1 @@ -1805,6 +1821,7 @@ class Slice(metaclass=utils.Singleton): self.buffer_slices["CORONAL"].discard_vtk_mask() self.buffer_slices["SAGITAL"].discard_vtk_mask() + self.current_mask.modified(target == '3D') Publisher.sendMessage("Reload actual slice") def calc_image_density(self, mask=None): diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index 1b8f7c8..922762c 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -1445,6 +1445,7 @@ class EditorInteractorStyle(DefaultInteractorStyle): self.viewer._flush_buffer = True self.viewer.slice_.apply_slice_buffer_to_mask(self.orientation) self.viewer._flush_buffer = False + self.viewer.slice_.current_mask.modified() def EOnScrollForward(self, evt, obj): iren = self.viewer.interactor @@ -1822,7 +1823,6 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): self.viewer.slice_.current_mask.matrix[0 , 0, n+1] markers = self.matrix[:, :, n] - ww = self.viewer.slice_.window_width wl = self.viewer.slice_.window_level @@ -1866,6 +1866,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.modified() self.viewer.slice_.current_mask.clear_history() # Marking the project as changed @@ -2017,10 +2018,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): mask[(tmp_mask==2) & ((mask == 0) | (mask == 2) | (mask == 253))] = 2 mask[(tmp_mask==1) & ((mask == 0) | (mask == 2) | (mask == 253))] = 253 - #mask[:] = tmp_mask - self.viewer.slice_.current_mask.matrix[0] = 1 - self.viewer.slice_.current_mask.matrix[:, 0, :] = 1 - self.viewer.slice_.current_mask.matrix[:, :, 0] = 1 + self.viewer.slice_.current_mask.modified(True) self.viewer.slice_.discard_all_buffers() self.viewer.slice_.current_mask.clear_history() @@ -2403,6 +2401,7 @@ class FloodFillMaskInteractorStyle(DefaultInteractorStyle): self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.modified(True) Publisher.sendMessage('Reload actual slice') @@ -2522,6 +2521,7 @@ class CropMaskInteractorStyle(DefaultInteractorStyle): self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.modified(True) Publisher.sendMessage('Reload actual slice') @@ -2713,6 +2713,7 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.modified(self.config.target == '3D') Publisher.sendMessage('Reload actual slice') def do_2d_seg(self): diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index b21454f..ce48e68 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -320,6 +320,9 @@ class Viewer(wx.Panel): Publisher.subscribe(self.UpdateMarkerOffsetPosition, 'Update marker offset') Publisher.subscribe(self.AddPeeledSurface, 'Update peel') + Publisher.subscribe(self.load_mask_preview, 'Load mask preview') + Publisher.subscribe(self.remove_mask_preview, 'Remove mask preview') + def SetStereoMode(self, mode): ren_win = self.interactor.GetRenderWindow() @@ -1663,6 +1666,19 @@ class Viewer(wx.Panel): # self._to_show_ball -= 1 # self._check_and_set_ball_visibility() + def load_mask_preview(self, mask_3d_actor, flag=True): + if flag: + self.ren.AddVolume(mask_3d_actor) + else: + self.ren.RemoveVolume(mask_3d_actor) + + if self.ren.GetActors().GetNumberOfItems() == 0 and self.ren.GetVolumes().GetNumberOfItems() == 1: + self.ren.ResetCamera() + self.ren.ResetCameraClippingRange() + + def remove_mask_preview(self, mask_3d_actor): + self.ren.RemoveVolume(mask_3d_actor) + def OnSetViewAngle(self, view): self.SetViewAngle(view) @@ -1893,11 +1909,9 @@ class SlicePlane: Publisher.sendMessage('Update slice 3D', widget=self.plane_z, orientation="AXIAL") - def DeletePlanes(self): del self.plane_x del self.plane_y - del self.plane_z - + del self.plane_z diff --git a/invesalius/data/volume.py b/invesalius/data/volume.py index 36cf178..1cbbebd 100644 --- a/invesalius/data/volume.py +++ b/invesalius/data/volume.py @@ -19,6 +19,7 @@ import plistlib import os import weakref +from distutils.version import LooseVersion import numpy import vtk @@ -707,9 +708,93 @@ class Volume(): #else: # valor = value return value - scale[0] - - -class CutPlane: + +class VolumeMask: + def __init__(self, mask): + self.mask = mask + self.colour = mask.colour + self._volume_mapper = None + self._flip = None + self._color_transfer = None + self._piecewise_function = None + self._actor = None + + def create_volume(self): + if self._actor is None: + if int(ses.Session().rendering) == 0: + self._volume_mapper = vtk.vtkFixedPointVolumeRayCastMapper() + #volume_mapper.AutoAdjustSampleDistancesOff() + self._volume_mapper.IntermixIntersectingGeometryOn() + pix_diag = 2.0 + self._volume_mapper.SetImageSampleDistance(0.25) + self._volume_mapper.SetSampleDistance(pix_diag / 5.0) + else: + self._volume_mapper = vtk.vtkGPUVolumeRayCastMapper() + self._volume_mapper.UseJitteringOn() + + if LooseVersion(vtk.vtkVersion().GetVTKVersion()) > LooseVersion('8.0'): + self._volume_mapper.SetBlendModeToIsoSurface() + + # else: + # isosurfaceFunc = vtk.vtkVolumeRayCastIsosurfaceFunction() + # isosurfaceFunc.SetIsoValue(127) + + # self._volume_mapper = vtk.vtkVolumeRayCastMapper() + # self._volume_mapper.SetVolumeRayCastFunction(isosurfaceFunc) + + self._flip = vtk.vtkImageFlip() + self._flip.SetInputData(self.mask.imagedata) + self._flip.SetFilteredAxis(1) + self._flip.FlipAboutOriginOn() + + self._volume_mapper.SetInputConnection(self._flip.GetOutputPort()) + self._volume_mapper.Update() + + r, g, b = self.colour + + self._color_transfer = vtk.vtkColorTransferFunction() + self._color_transfer.RemoveAllPoints() + self._color_transfer.AddRGBPoint(0.0, 0, 0, 0) + self._color_transfer.AddRGBPoint(254.0, r, g, b) + self._color_transfer.AddRGBPoint(255.0, r, g, b) + + self._piecewise_function = vtk.vtkPiecewiseFunction() + self._piecewise_function.RemoveAllPoints() + self._piecewise_function.AddPoint(0.0, 0.0) + self._piecewise_function.AddPoint(127, 1.0) + + self._volume_property = vtk.vtkVolumeProperty() + self._volume_property.SetColor(self._color_transfer) + self._volume_property.SetScalarOpacity(self._piecewise_function) + self._volume_property.ShadeOn() + self._volume_property.SetInterpolationTypeToLinear() + #vp.SetSpecular(1.75) + #vp.SetSpecularPower(8) + + if not self._volume_mapper.IsA("vtkGPUVolumeRayCastMapper"): + self._volume_property.SetScalarOpacityUnitDistance(pix_diag) + else: + if LooseVersion(vtk.vtkVersion().GetVTKVersion()) > LooseVersion('8.0'): + self._volume_property.GetIsoSurfaceValues().SetValue(0, 127) + + self._actor = vtk.vtkVolume() + self._actor.SetMapper(self._volume_mapper) + self._actor.SetProperty(self._volume_property) + self._actor.Update() + + def change_imagedata(self): + self._flip.SetInputData(self.mask.imagedata) + + def set_colour(self, colour): + self.colour = colour + r, g, b = self.colour + self._color_transfer.RemoveAllPoints() + self._color_transfer.AddRGBPoint(0.0, 0, 0, 0) + self._color_transfer.AddRGBPoint(254.0, r, g, b) + self._color_transfer.AddRGBPoint(255.0, r, g, b) + + +class CutPlane: def __init__(self, img, volume_mapper): self.img = img self.volume_mapper = volume_mapper diff --git a/invesalius/gui/data_notebook.py b/invesalius/gui/data_notebook.py index 91b61e2..ed22deb 100644 --- a/invesalius/gui/data_notebook.py +++ b/invesalius/gui/data_notebook.py @@ -576,12 +576,9 @@ class MasksListCtrlPanel(InvListCtrl): Publisher.sendMessage('Show mask', index=index, value=flag) - - def InsertNewItem(self, index=0, label=_("Mask"), threshold="(1000, 4500)", - colour=None): + def InsertNewItem(self, index=0, label=_("Mask"), threshold="(1000, 4500)", colour=None): self.InsertItem(index, "") - self.SetItem(index, 1, label, - imageId=self.mask_list_index[index]) + self.SetItem(index, 1, label, imageId=self.mask_list_index[index]) self.SetItem(index, 2, threshold) # self.SetItemImage(index, 1) # for key in self.mask_list_index.keys(): diff --git a/invesalius/gui/default_viewers.py b/invesalius/gui/default_viewers.py index 1f31287..3ad8fdd 100644 --- a/invesalius/gui/default_viewers.py +++ b/invesalius/gui/default_viewers.py @@ -319,7 +319,6 @@ import wx.lib.buttons as btn from pubsub import pub as Publisher import wx.lib.colourselect as csel -[BUTTON_RAYCASTING, BUTTON_VIEW, BUTTON_SLICE_PLANE, BUTTON_3D_STEREO, BUTTON_TARGET] = [wx.NewId() for num in range(5)] RAYCASTING_TOOLS = wx.NewId() ID_TO_NAME = {} @@ -330,6 +329,8 @@ ID_TO_ITEMSLICEMENU = {} ID_TO_ITEM_3DSTEREO = {} ID_TO_STEREO_NAME = {} +ICON_SIZE = (32, 32) + class VolumeViewerCover(wx.Panel): def __init__(self, parent): @@ -349,47 +350,22 @@ class VolumeToolPanel(wx.Panel): wx.Panel.__init__(self, parent) # VOLUME RAYCASTING BUTTON - BMP_RAYCASTING = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "volume_raycasting.png"), - wx.BITMAP_TYPE_PNG) - - BMP_SLICE_PLANE = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "slice_plane.png"), - wx.BITMAP_TYPE_PNG) - - - BMP_3D_STEREO = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "3D_glasses.png"), - wx.BITMAP_TYPE_PNG) - - BMP_TARGET = wx.Bitmap(os.path.join(inv_paths.ICON_DIR, "target.png"), - wx.BITMAP_TYPE_PNG) - - - button_raycasting = pbtn.PlateButton(self, BUTTON_RAYCASTING,"", - BMP_RAYCASTING, style=pbtn.PB_STYLE_SQUARE, - size=(32,32)) - - button_stereo = pbtn.PlateButton(self, BUTTON_3D_STEREO,"", - BMP_3D_STEREO, style=pbtn.PB_STYLE_SQUARE, - size=(32,32)) - - button_slice_plane = self.button_slice_plane = pbtn.PlateButton(self, BUTTON_SLICE_PLANE,"", - BMP_SLICE_PLANE, style=pbtn.PB_STYLE_SQUARE, - size=(32,32)) - - button_target = self.button_target = pbtn.PlateButton(self, BUTTON_TARGET,"", - BMP_TARGET, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, - size=(32,32)) + BMP_RAYCASTING = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("volume_raycasting.png")), wx.BITMAP_TYPE_PNG) + BMP_SLICE_PLANE = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("slice_plane.png")), wx.BITMAP_TYPE_PNG) + BMP_3D_STEREO = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("3D_glasses.png")), wx.BITMAP_TYPE_PNG) + BMP_TARGET = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("target.png")), wx.BITMAP_TYPE_PNG) + BMP_3D_MASK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("file_from_internet.png")), wx.BITMAP_TYPE_PNG) + + self.button_raycasting = pbtn.PlateButton(self, -1,"", BMP_RAYCASTING, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + self.button_stereo = pbtn.PlateButton(self, -1,"", BMP_3D_STEREO, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + self.button_slice_plane = pbtn.PlateButton(self, -1, "", BMP_SLICE_PLANE, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + self.button_target = pbtn.PlateButton(self, -1,"", BMP_TARGET, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, size=ICON_SIZE) self.button_target.Enable(0) - - self.button_raycasting = button_raycasting - self.button_stereo = button_stereo + # self.button_3d_mask = pbtn.PlateButton(self, -1, "", BMP_3D_MASK, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, size=ICON_SIZE) # VOLUME VIEW ANGLE BUTTON - BMP_FRONT = wx.Bitmap(ID_TO_BMP[const.VOL_FRONT][1], - wx.BITMAP_TYPE_PNG) - button_view = pbtn.PlateButton(self, BUTTON_VIEW, "", - BMP_FRONT, size=(32,32), - style=pbtn.PB_STYLE_SQUARE) - self.button_view = button_view + BMP_FRONT = wx.Bitmap(ID_TO_BMP[const.VOL_FRONT][1], wx.BITMAP_TYPE_PNG) + self.button_view = pbtn.PlateButton(self, -1, "", BMP_FRONT, size=(32,32), style=pbtn.PB_STYLE_SQUARE) # VOLUME COLOUR BUTTON if sys.platform.startswith('linux'): @@ -399,18 +375,17 @@ class VolumeToolPanel(wx.Panel): size = (24,24) sp = 5 - button_colour= csel.ColourSelect(self, 111,colour=(0,0,0), - size=size) - self.button_colour = button_colour + self.button_colour= csel.ColourSelect(self, -1, colour=(0,0,0), size=size) # SIZER TO ORGANIZE ALL sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(button_colour, 0, wx.ALL, sp) - sizer.Add(button_raycasting, 0, wx.TOP|wx.BOTTOM, 1) - sizer.Add(button_view, 0, wx.TOP|wx.BOTTOM, 1) - sizer.Add(button_slice_plane, 0, wx.TOP|wx.BOTTOM, 1) - sizer.Add(button_stereo, 0, wx.TOP|wx.BOTTOM, 1) - sizer.Add(button_target, 0, wx.TOP | wx.BOTTOM, 1) + sizer.Add(self.button_colour, 0, wx.ALL, sp) + sizer.Add(self.button_raycasting, 0, wx.TOP|wx.BOTTOM, 1) + sizer.Add(self.button_view, 0, wx.TOP|wx.BOTTOM, 1) + sizer.Add(self.button_slice_plane, 0, wx.TOP|wx.BOTTOM, 1) + sizer.Add(self.button_stereo, 0, wx.TOP|wx.BOTTOM, 1) + sizer.Add(self.button_target, 0, wx.TOP | wx.BOTTOM, 1) + # sizer.Add(self.button_3d_mask, 0, wx.TOP | wx.BOTTOM, 1) self.navigation_status = False self.status_target_select = False diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 003e9f0..2426c7c 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -118,6 +118,7 @@ class Frame(wx.Frame): self.actived_interpolated_slices = main_menu.view_menu self.actived_navigation_mode = main_menu.mode_menu self.actived_dbs_mode = main_menu.mode_dbs + self.tools_menu = main_menu.tools_menu # Set menus, status and task bar self.SetMenuBar(main_menu) @@ -538,6 +539,15 @@ class Frame(wx.Frame): elif id == const.ID_CROP_MASK: self.OnCropMask() + elif id == const.ID_MASK_3D_PREVIEW: + self.OnEnableMask3DPreview(value=self.tools_menu.IsChecked(const.ID_MASK_3D_PREVIEW)) + + elif id == const.ID_MASK_3D_AUTO_RELOAD: + ses.Session().auto_reload_preview = self.tools_menu.IsChecked(const.ID_MASK_3D_AUTO_RELOAD) + + elif id == const.ID_MASK_3D_RELOAD: + self.OnUpdateMaskPreview() + elif id == const.ID_CREATE_SURFACE: Publisher.sendMessage('Open create surface dialog') @@ -769,6 +779,15 @@ class Frame(wx.Frame): def OnCropMask(self): Publisher.sendMessage('Enable style', style=const.SLICE_STATE_CROP_MASK) + def OnEnableMask3DPreview(self, value): + if value: + Publisher.sendMessage('Enable mask 3D preview') + else: + Publisher.sendMessage('Disable mask 3D preview') + + def OnUpdateMaskPreview(self): + Publisher.sendMessage('Update mask 3D preview') + def ShowPluginsFolder(self): """ Show getting started window. @@ -954,6 +973,22 @@ class MenuBar(wx.MenuBar): self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop")) self.crop_mask_menu.Enable(False) + mask_menu.AppendSeparator() + + mask_preview_menu = wx.Menu() + + self.mask_preview = mask_preview_menu.Append(const.ID_MASK_3D_PREVIEW, _("Enable") + "\tCtrl+Shift+M", "", wx.ITEM_CHECK) + self.mask_preview.Enable(False) + + self.mask_auto_reload = mask_preview_menu.Append(const.ID_MASK_3D_AUTO_RELOAD, _("Auto reload") + "\tCtrl+Shift+U", "", wx.ITEM_CHECK) + self.mask_auto_reload.Check(ses.Session().auto_reload_preview) + self.mask_auto_reload.Enable(False) + + self.mask_preview_reload = mask_preview_menu.Append(const.ID_MASK_3D_RELOAD, _("Reload") + "\tCtrl+Shift+R") + self.mask_preview_reload.Enable(False) + + mask_menu.Append(-1, _('Mask 3D Preview'), mask_preview_menu) + # Segmentation Menu segmentation_menu = wx.Menu() self.threshold_segmentation = segmentation_menu.Append(const.ID_THRESHOLD_SEGMENTATION, _(u"Threshold\tCtrl+Shift+T")) @@ -995,6 +1030,7 @@ class MenuBar(wx.MenuBar): tools_menu.Append(-1, _(u"Mask"), mask_menu) tools_menu.Append(-1, _(u"Segmentation"), segmentation_menu) tools_menu.Append(-1, _(u"Surface"), surface_menu) + self.tools_menu = tools_menu #View self.view_menu = view_menu = wx.Menu() @@ -1200,6 +1236,9 @@ class MenuBar(wx.MenuBar): def OnAddMask(self, mask): self.num_masks += 1 self.bool_op_menu.Enable(self.num_masks >= 2) + self.mask_preview.Enable(True) + self.mask_auto_reload.Enable(True) + self.mask_preview_reload.Enable(True) def OnRemoveMasks(self, mask_indexes): self.num_masks -= len(mask_indexes) @@ -1208,6 +1247,9 @@ class MenuBar(wx.MenuBar): def OnShowMask(self, index, value): self.clean_mask_menu.Enable(value) self.crop_mask_menu.Enable(value) + self.mask_preview.Enable(value) + self.mask_auto_reload.Enable(value) + self.mask_preview_reload.Enable(value) # ------------------------------------------------------------------ diff --git a/invesalius/project.py b/invesalius/project.py index 62ea790..111032a 100644 --- a/invesalius/project.py +++ b/invesalius/project.py @@ -120,6 +120,8 @@ class Project(metaclass=Singleton): def RemoveMask(self, index): new_dict = {} for i in self.mask_dict: + mask = self.mask_dict[i] + mask.cleanup() if i < index: new_dict[i] = self.mask_dict[i] if i > index: @@ -331,6 +333,7 @@ class Project(metaclass=Singleton): filename = project["masks"][index] filepath = os.path.join(dirpath, filename) m = msk.Mask() + m.spacing = self.spacing m.OpenPList(filepath) self.mask_dict[m.index] = m diff --git a/invesalius/segmentation/brain/segment.py b/invesalius/segmentation/brain/segment.py index 741d95e..3dd2fd8 100644 --- a/invesalius/segmentation/brain/segment.py +++ b/invesalius/segmentation/brain/segment.py @@ -175,8 +175,8 @@ class SegmentProcess(ctx.Process): self.mask = slc.Slice().create_new_mask(name=name) self.mask.was_edited = True - self.mask.matrix[:] = 1 self.mask.matrix[1:, 1:, 1:] = (self._probability_array >= threshold) * 255 + self.mask.modified(True) def get_completion(self): return self._comm_array[0] diff --git a/invesalius/session.py b/invesalius/session.py index 9c9901d..e8acdd8 100644 --- a/invesalius/session.py +++ b/invesalius/session.py @@ -34,7 +34,7 @@ import json from pubsub import pub as Publisher import wx -from invesalius.utils import Singleton, debug, decode +from invesalius.utils import Singleton, debug, decode, deep_merge_dict from random import randint from invesalius import inv_paths @@ -59,6 +59,7 @@ class Session(metaclass=Singleton): 'session': { 'status': 3, 'language': '', + 'auto_reload_preview': False, }, 'project': { }, @@ -76,6 +77,7 @@ class Session(metaclass=Singleton): 'surface_interpolation': ('session', 'surface_interpolation'), 'rendering': ('session', 'rendering'), 'slice_interpolation': ('session', 'slice_interpolation'), + 'auto_reload_preview': ('session', 'auto_reload_preview'), 'recent_projects': ('project', 'recent_projects'), 'homedir': ('paths', 'homedir'), 'tempdir': ('paths', 'homedir'), @@ -95,6 +97,7 @@ class Session(metaclass=Singleton): 'surface_interpolation': 1, 'rendering': 0, 'slice_interpolation': 0, + 'auto_reload_preview': False, }, 'project': { @@ -268,7 +271,8 @@ class Session(metaclass=Singleton): def _read_cfg_from_json(self, json_filename): with open(json_filename, 'r') as cfg_file: cfg_dict = json.load(cfg_file) - self._values.update(cfg_dict) + self._value = deep_merge_dict(self._values, cfg_dict) + print(self._values) # Do not reading project status from the config file, since there # isn't a recover session tool in InVesalius yet. diff --git a/invesalius/utils.py b/invesalius/utils.py index 9098046..99a4cba 100644 --- a/invesalius/utils.py +++ b/invesalius/utils.py @@ -23,6 +23,7 @@ import re import locale import math import traceback +import collections.abc from distutils.version import LooseVersion from functools import wraps @@ -487,3 +488,13 @@ def log_traceback(ex): tb_lines = [line.rstrip('\n') for line in traceback.format_exception(ex.__class__, ex, ex_traceback)] return ''.join(tb_lines) + + + +def deep_merge_dict(d, u): + for k, v in u.items(): + if isinstance(v, collections.abc.Mapping): + d[k] = deep_merge_dict(d.get(k, {}), v) + else: + d[k] = v + return d -- libgit2 0.21.2