From c79f0659a38f58644ae9feda327f828b704f2f2c Mon Sep 17 00:00:00 2001 From: Thiago Franco de Moraes Date: Thu, 25 Aug 2016 13:11:23 -0300 Subject: [PATCH] Select mask part tool (#50) --- invesalius/constants.py | 4 ++++ invesalius/data/mask.py | 17 +++++++++++------ invesalius/data/slice_.py | 54 +++++++++++++++++++++++++++++++++++++++++------------- invesalius/data/styles.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/data/viewer_slice.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ invesalius/gui/data_notebook.py | 10 +++++----- invesalius/gui/dialogs.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/gui/frame.py | 22 ++++++++++++++-------- invesalius/gui/task_slice.py | 8 ++++---- 9 files changed, 310 insertions(+), 42 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index c9fe27e..bc92fbd 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -483,6 +483,7 @@ ID_CLEAN_MASK = wx.NewId() ID_REORIENT_IMG = wx.NewId() ID_FLOODFILL_MASK = wx.NewId() ID_REMOVE_MASK_PART = wx.NewId() +ID_SELECT_MASK_PART = wx.NewId() #--------------------------------------------------------- STATE_DEFAULT = 1000 @@ -502,6 +503,7 @@ SLICE_STATE_WATERSHED = 3009 SLICE_STATE_REORIENT = 3010 SLICE_STATE_MASK_FFILL = 3011 SLICE_STATE_REMOVE_MASK_PARTS = 3012 +SLICE_STATE_SELECT_MASK_PARTS = 3013 VOLUME_STATE_SEED = 2001 # STATE_LINEAR_MEASURE = 3001 @@ -521,6 +523,7 @@ SLICE_STYLES.append(SLICE_STATE_EDITOR) SLICE_STYLES.append(SLICE_STATE_WATERSHED) SLICE_STYLES.append(SLICE_STATE_MASK_FFILL) SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS) +SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS) VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, STATE_MEASURE_ANGLE] @@ -531,6 +534,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, SLICE_STATE_WATERSHED: 1, SLICE_STATE_MASK_FFILL: 2, SLICE_STATE_REMOVE_MASK_PARTS: 2, + SLICE_STATE_SELECT_MASK_PARTS: 2, SLICE_STATE_CROSS: 2, SLICE_STATE_SCROLL: 2, SLICE_STATE_REORIENT: 2, diff --git a/invesalius/data/mask.py b/invesalius/data/mask.py index 96df501..c204cb9 100644 --- a/invesalius/data/mask.py +++ b/invesalius/data/mask.py @@ -126,7 +126,7 @@ class EditionHistory(object): h[self.index].commit_history(mvolume) self._reload_slice(self.index) Publisher.sendMessage("Enable redo", True) - + if self.index == 0: Publisher.sendMessage("Enable undo", False) print "AT", self.index, len(self.history), self.history[self.index].filename @@ -154,7 +154,7 @@ class EditionHistory(object): h[self.index].commit_history(mvolume) self._reload_slice(self.index) Publisher.sendMessage("Enable undo", True) - + if self.index == len(h) - 1: Publisher.sendMessage("Enable redo", False) print "AT", self.index, len(h), h[self.index].filename @@ -174,7 +174,7 @@ class EditionHistory(object): v_undo = False elif self.index == len(self.history) - 1: v_redo = False - + Publisher.sendMessage("Enable undo", v_undo) Publisher.sendMessage("Enable redo", v_redo) @@ -229,7 +229,7 @@ class Mask(): def SavePlist(self, dir_temp, filelist): mask = {} - filename = u'mask_%d' % self.index + filename = u'mask_%d' % self.index mask_filename = u'%s.dat' % filename mask_filepath = os.path.join(dir_temp, mask_filename) filelist[self.temp_file] = mask_filename @@ -304,7 +304,12 @@ class Mask(): Mask.general_index = index def create_mask(self, shape): - print "Creating a mask" + """ + Creates a new mask object. This method do not append this new mask into the project. + + Parameters: + shape(int, int, int): The shape of the new mask. + """ self.temp_file = tempfile.mktemp() shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 self.matrix = numpy.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) @@ -329,7 +334,7 @@ class Mask(): new_mask.threshold_range = self.threshold_range new_mask.edition_threshold_range = self.edition_threshold_range new_mask.is_shown = self.is_shown - + new_mask.create_mask(shape=[i-1 for i in self.matrix.shape]) new_mask.matrix[:] = self.matrix[:] diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index 1adead7..f3d377d 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -302,14 +302,14 @@ class Slice(object): def __add_mask(self, pubsub_evt): mask_name = pubsub_evt.data - self.CreateMask(name=mask_name) + self.create_new_mask(name=mask_name) self.SetMaskColour(self.current_mask.index, self.current_mask.colour) def __add_mask_thresh(self, pubsub_evt): mask_name = pubsub_evt.data[0] thresh = pubsub_evt.data[1] colour = pubsub_evt.data[2] - self.CreateMask(name=mask_name, threshold_range=thresh, colour =colour) + self.create_new_mask(name=mask_name, threshold_range=thresh, colour=colour) self.SetMaskColour(self.current_mask.index, self.current_mask.colour) self.SelectCurrentMask(self.current_mask.index) Publisher.sendMessage('Reload actual slice') @@ -574,6 +574,14 @@ class Slice(object): 1: (0.0, 1.0, 0.0, 1.0), 2: (1.0, 0.0, 0.0, 1.0)}) final_image = self.do_blend(final_image, cimage) + elif self.to_show_aux and self.current_mask: + m = self.get_aux_slice(self.to_show_aux, orientation, slice_number) + tmp_vimage = converters.to_vtk(m, self.spacing, slice_number, orientation) + aux_image = self.do_custom_colour(tmp_vimage, {0: (0.0, 0.0, 0.0, 0.0), + 1: (0.0, 0.0, 0.0, 0.0), + 254: (1.0, 0.0, 0.0, 1.0), + 255: (1.0, 0.0, 0.0, 1.0)}) + final_image = self.do_blend(final_image, aux_image) return final_image def get_image_slice(self, orientation, slice_number, number_slices=1, @@ -1026,14 +1034,32 @@ class Slice(object): #else: #widget.SetInput(cast.GetOutput()) - - - def CreateMask(self, imagedata=None, name=None, colour=None, - opacity=None, threshold_range=None, - edition_threshold_range = None, - edited_points=None): - - # TODO: mask system to new system. + def create_new_mask(self, name=None, + colour=None, + opacity=None, + threshold_range=None, + edition_threshold_range=None, + add_to_project=True, + show=True): + """ + Creates a new mask and add it to project. + + Parameters: + name (string): name of the new mask. If name is None a automatic + name will be used. + colour (R, G, B): a RGB tuple of float number. + opacity (float): a float number, from 0 to 1. If opacity is None + the default one will be used. + threshold_range (int, int): a 2-tuple indicating threshold range. + If None the default one will be used. + edition_threshold_range (int, int): a 2-tuple indicating threshold + range. If None the default one will be used. + show (bool): if this new mask will be showed and set as current + mask. + + Returns: + new_mask: The new mask object. + """ future_mask = Mask() future_mask.create_mask(self.matrix.shape) @@ -1045,12 +1071,13 @@ class Slice(object): future_mask.opacity = opacity if edition_threshold_range: future_mask.edition_threshold_range = edition_threshold_range - if edited_points: - future_mask.edited_points = edited_points if threshold_range: future_mask.threshold_range = threshold_range - self._add_mask_into_proj(future_mask) + if add_to_project: + self._add_mask_into_proj(future_mask, show=show) + + return future_mask def _add_mask_into_proj(self, mask, show=True): @@ -1282,6 +1309,7 @@ class Slice(object): op, m1, m2 = pubsub_evt.data self.do_boolean_op(op, m1, m2) + def do_boolean_op(self, op, m1, m2): name_ops = {const.BOOLEAN_UNION: _(u"Union"), const.BOOLEAN_DIFF: _(u"Diff"), diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index d22295b..8f00776 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -1928,6 +1928,98 @@ class RemoveMaskPartsInteractorStyle(FloodFillMaskInteractorStyle): self._progr_msg = _(u"Removing part ...") +class SelectPartConfig(object): + __metaclass__= utils.Singleton + def __init__(self): + self.mask = None + self.con_3d = 6 + self.dlg_visible = False + self.mask_name = '' + + +class SelectMaskPartsInteractorStyle(DefaultInteractorStyle): + def __init__(self, viewer): + DefaultInteractorStyle.__init__(self, viewer) + + self.viewer = viewer + self.orientation = self.viewer.orientation + + self.picker = vtk.vtkWorldPointPicker() + self.slice_actor = viewer.slice_data.actor + self.slice_data = viewer.slice_data + + self.config = SelectPartConfig() + self.dlg = None + + self.t0 = 254 + self.t1 = 255 + self.fill_value = 254 + + self.AddObserver("LeftButtonPressEvent", self.OnSelect) + + def SetUp(self): + if not self.config.dlg_visible: + import data.mask as mask + default_name = const.MASK_NAME_PATTERN %(mask.Mask.general_index+2) + + self.config.mask_name = default_name + self.config.dlg_visible = True + self.dlg= dialogs.SelectPartsOptionsDialog(self.config) + self.dlg.Show() + + def CleanUp(self): + if (self.dlg is not None) and (self.config.dlg_visible): + self.config.dlg_visible = False + self.dlg.Destroy() + self.dlg = None + + if self.config.mask: + self.config.mask.name = self.config.mask_name + self.viewer.slice_._add_mask_into_proj(self.config.mask) + self.viewer.slice_.SelectCurrentMask(self.config.mask.index) + Publisher.sendMessage('Change mask selected', self.config.mask.index) + self.config.mask = None + del self.viewer.slice_.aux_matrices['SELECT'] + self.viewer.slice_.to_show_aux = '' + Publisher.sendMessage('Reload actual slice') + + def OnSelect(self, obj, evt): + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + iren = self.viewer.interactor + mouse_x, mouse_y = iren.GetEventPosition() + x, y, z = self.viewer.get_voxel_clicked(mouse_x, mouse_y, self.picker) + + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + + bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') + self.viewer.slice_.do_threshold_to_all_slices() + + if self.config.mask is None: + self._create_new_mask() + + if iren.GetControlKey(): + floodfill.floodfill_threshold(self.config.mask.matrix[1:, 1:, 1:], [[x, y, z]], 254, 255, 0, bstruct, self.config.mask.matrix[1:, 1:, 1:]) + else: + floodfill.floodfill_threshold(mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, self.config.mask.matrix[1:, 1:, 1:]) + + self.viewer.slice_.aux_matrices['SELECT'] = self.config.mask.matrix[1:, 1:, 1:] + self.viewer.slice_.to_show_aux = 'SELECT' + + self.config.mask.was_edited = True + Publisher.sendMessage('Reload actual slice') + + def _create_new_mask(self): + mask = self.viewer.slice_.create_new_mask(show=False, add_to_project=False) + mask.was_edited = True + mask.matrix[0, :, :] = 1 + mask.matrix[:, 0, :] = 1 + mask.matrix[:, :, 0] = 1 + + self.config.mask = mask + + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, @@ -1945,6 +2037,7 @@ def get_style(style): const.SLICE_STATE_REORIENT: ReorientImageInteractorStyle, const.SLICE_STATE_MASK_FFILL: FloodFillMaskInteractorStyle, const.SLICE_STATE_REMOVE_MASK_PARTS: RemoveMaskPartsInteractorStyle, + const.SLICE_STATE_SELECT_MASK_PARTS: SelectMaskPartsInteractorStyle, } return STYLES[style] diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index 5299af2..581efb9 100755 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -977,9 +977,12 @@ class Viewer(wx.Panel): my = round((z - zi)/self.slice_.spacing[2], 0) return my, mx - def get_coordinate_cursor(self): + def get_coordinate_cursor(self, picker=None): # Find position - x, y, z = self.pick.GetPickPosition() + if picker is None: + picker = self.pick + + x, y, z = picker.GetPickPosition() bounds = self.slice_data.actor.GetBounds() if bounds[0] == bounds[1]: x = bounds[0] @@ -989,11 +992,14 @@ class Viewer(wx.Panel): z = bounds[4] return x, y, z - def get_coordinate_cursor_edition(self, slice_data): + def get_coordinate_cursor_edition(self, slice_data, picker=None): # Find position actor = slice_data.actor slice_number = slice_data.number - x, y, z = self.pick.GetPickPosition() + if picker is None: + picker = self.pick + + x, y, z = picker.GetPickPosition() # First we fix the position origin, based on vtkActor bounds bounds = actor.GetBounds() @@ -1023,6 +1029,41 @@ class Viewer(wx.Panel): return x, y, z + def get_voxel_clicked(self, mx, my, picker=None): + """ + Given the (mx, my) mouse clicked position returns the voxel coordinate + of the voxel at (that mx, my) position. + + Parameters: + mx (int): x position. + my (int): y position + picker: the picker used to get calculate the voxel coordinate. + + Returns: + voxel_coordinate (x, y, z): voxel coordinate inside the matrix. Can + be used to access the voxel value inside the matrix. + """ + if picker is None: + picker = self.pick + + slice_data = self.slice_data + renderer = slice_data.renderer + + picker.Pick(mx, my, 0, renderer) + + coord = self.get_coordinate_cursor(picker) + position = slice_data.actor.GetInput().FindPoint(coord) + + if position != -1: + coord = slice_data.actor.GetInput().GetPoint(position) + + if position < 0: + position = viewer.calculate_matrix_position(coord) + + x, y, z = self.calcultate_scroll_position(position) + + return (x, y, z) + def __bind_events(self): Publisher.subscribe(self.LoadImagedata, 'Load slice to viewer') @@ -1044,8 +1085,8 @@ class Viewer(wx.Panel): Publisher.subscribe(self.Navigation, 'Co-registered Points') ### - Publisher.subscribe(self.ChangeBrushColour, - 'Add mask') + # Publisher.subscribe(self.ChangeBrushColour, + # 'Add mask') Publisher.subscribe(self.UpdateWindowLevelValue, 'Update window level value') diff --git a/invesalius/gui/data_notebook.py b/invesalius/gui/data_notebook.py index a63ee53..59f3d62 100644 --- a/invesalius/gui/data_notebook.py +++ b/invesalius/gui/data_notebook.py @@ -512,11 +512,11 @@ class MasksListCtrlPanel(wx.ListCtrl, listmix.TextEditMixin): self.SetStringItem(index, 1, label, imageId=self.mask_list_index[index]) self.SetStringItem(index, 2, threshold) - self.SetItemImage(index, 1) - for key in self.mask_list_index.keys(): - if key != index: - self.SetItemImage(key, 0) - self.current_index = index + # self.SetItemImage(index, 1) + # for key in self.mask_list_index.keys(): + # if key != index: + # self.SetItemImage(key, 0) + # self.current_index = index def AddMask(self, pubsub_evt): index, mask_name, threshold_range, colour = pubsub_evt.data diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 2578ab7..0fc4dd4 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1940,3 +1940,94 @@ class FFillOptionsDialog(wx.Dialog): Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL) evt.Skip() self.Destroy() + + +class SelectPartsOptionsDialog(wx.Dialog): + def __init__(self, config): + pre = wx.PreDialog() + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Select mask parts"), style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) + self.PostCreate(pre) + + self.config = config + + self._init_gui() + + def _init_gui(self): + self.target_name = wx.TextCtrl(self, -1) + self.target_name.SetValue(self.config.mask_name) + + # Connectivity 3D + self.conect3D_6 = wx.RadioButton(self, -1, "6", style=wx.RB_GROUP) + self.conect3D_18 = wx.RadioButton(self, -1, "18") + self.conect3D_26 = wx.RadioButton(self, -1, "26") + + if self.config.con_3d == 18: + self.conect3D_18.SetValue(1) + elif self.config.con_3d == 26: + self.conect3D_26.SetValue(1) + else: + self.conect3D_6.SetValue(1) + + sizer_t = wx.BoxSizer(wx.HORIZONTAL) + sizer_t.AddSpacer(7) + sizer_t.Add(wx.StaticText(self, -1, _(u"Target mask name")), 1, wx.ALIGN_CENTRE_VERTICAL) + sizer_t.AddSpacer(7) + sizer_t.Add(self.target_name, 1, wx.EXPAND) + sizer_t.AddSpacer(7) + + sizer_c = wx.BoxSizer(wx.HORIZONTAL) + sizer_c.AddSpacer(7) + sizer_c.Add(self.conect3D_6) + sizer_c.AddSpacer(7) + sizer_c.Add(self.conect3D_18) + sizer_c.AddSpacer(7) + sizer_c.Add(self.conect3D_26) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.AddSpacer(7) + sizer.Add(sizer_t, 1, wx.EXPAND) + sizer.AddSpacer(7) + sizer.Add(wx.StaticText(self, -1, _(u"3D Connectivity")), 0, wx.LEFT, 7) + sizer.AddSpacer(5) + sizer.Add(sizer_c) + sizer.AddSpacer(7) + + # sizer = wx.GridBagSizer(11, 6) + # sizer.AddStretchSpacer((0, 0)) + + # sizer.Add(wx.StaticText(self, -1, _(u"Target mask name")), (1, 0), (1, 6), flag=wx.LEFT|wx.ALIGN_BOTTOM|wx.EXPAND, border=7) + # sizer.Add(self.target_name, (2, 0), (1, 6), flag=wx.LEFT|wx.EXPAND|wx.RIGHT|wx.ALIGN_TOP, border=9) + + # # sizer.AddStretchSpacer((3, 0)) + + # sizer.Add(wx.StaticText(self, -1, _(u"3D Connectivity")), (3, 0), (1, 6), flag=wx.LEFT, border=7) + # sizer.Add(self.conect3D_6, (4, 0), flag=wx.LEFT, border=9) + # sizer.Add(self.conect3D_18, (4, 1), flag=wx.LEFT, border=9) + # sizer.Add(self.conect3D_26, (4, 2), flag=wx.LEFT, border=9) + # sizer.AddStretchSpacer((5, 0)) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() + + self.target_name.Bind(wx.EVT_CHAR, self.OnChar) + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetRadio) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + def OnChar(self, evt): + evt.Skip() + self.config.mask_name = self.target_name.GetValue() + + def OnSetRadio(self, evt): + if self.conect3D_6.GetValue(): + self.config.con_3d = 6 + elif self.conect3D_18.GetValue(): + self.config.con_3d = 18 + elif self.conect3D_26.GetValue(): + self.config.con_3d = 26 + + def OnClose(self, evt): + if self.config.dlg_visible: + Publisher.sendMessage('Disable style', const.SLICE_STATE_SELECT_MASK_PARTS) + evt.Skip() + self.Destroy() diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 4d54aed..3089316 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -451,20 +451,16 @@ class Frame(wx.Frame): elif id == const.ID_REMOVE_MASK_PART: self.OnRemoveMaskParts() + elif id == const.ID_SELECT_MASK_PART: + self.OnSelectMaskParts() + elif id == const.ID_VIEW_INTERPOLATED: - st = self.actived_interpolated_slices.IsChecked(const.ID_VIEW_INTERPOLATED) - if st: self.OnInterpolatedSlices(True) else: self.OnInterpolatedSlices(False) - def OnInterpolatedSlices(self, status): - - Publisher.sendMessage('Set interpolated slices', status) - - def OnSize(self, evt): """ Refresh GUI when frame is resized. @@ -594,6 +590,12 @@ class Frame(wx.Frame): def OnRemoveMaskParts(self): Publisher.sendMessage('Enable style', const.SLICE_STATE_REMOVE_MASK_PARTS) + def OnSelectMaskParts(self): + Publisher.sendMessage('Enable style', const.SLICE_STATE_SELECT_MASK_PARTS) + + def OnInterpolatedSlices(self, status): + Publisher.sendMessage('Set interpolated slices', status) + # ------------------------------------------------------------------ # ------------------------------------------------------------------ # ------------------------------------------------------------------ @@ -615,7 +617,8 @@ class MenuBar(wx.MenuBar): const.ID_PROJECT_CLOSE, const.ID_REORIENT_IMG, const.ID_FLOODFILL_MASK, - const.ID_REMOVE_MASK_PART,] + const.ID_REMOVE_MASK_PART, + const.ID_SELECT_MASK_PART,] self.__init_items() self.__bind_events() @@ -733,6 +736,9 @@ class MenuBar(wx.MenuBar): self.remove_mask_part_menu = mask_menu.Append(const.ID_REMOVE_MASK_PART, _(u"Remove parts")) self.remove_mask_part_menu.Enable(False) + self.select_mask_part_menu = mask_menu.Append(const.ID_SELECT_MASK_PART, _(u"Select parts")) + self.select_mask_part_menu.Enable(False) + tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) # Image menu diff --git a/invesalius/gui/task_slice.py b/invesalius/gui/task_slice.py index 2e74870..953b743 100644 --- a/invesalius/gui/task_slice.py +++ b/invesalius/gui/task_slice.py @@ -562,10 +562,10 @@ class MaskProperties(wx.Panel): mask_thresh = evt_pubsub.data[2] mask_colour = [int(c*255) for c in evt_pubsub.data[3]] index = self.combo_mask_name.Append(mask_name) - self.combo_mask_name.SetSelection(index) - self.button_colour.SetColour(mask_colour) - self.gradient.SetColour(mask_colour) - self.combo_mask_name.SetSelection(index) + # self.combo_mask_name.SetSelection(index) + # self.button_colour.SetColour(mask_colour) + # self.gradient.SetColour(mask_colour) + # self.combo_mask_name.SetSelection(index) def GetMaskSelected(self): x = self.combo_mask_name.GetSelection() -- libgit2 0.21.2