From d54e05ee97b91d31a931b4ceb6ec2b7ea6face14 Mon Sep 17 00:00:00 2001 From: Thiago Franco de Moraes Date: Tue, 30 Aug 2016 11:49:21 -0300 Subject: [PATCH] Segmenting using floodfill --- invesalius/constants.py | 4 ++++ invesalius/data/styles.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/gui/dialogs.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/gui/frame.py | 18 +++++++++++++++--- invesalius/gui/widgets/gradient.py | 1 + 5 files changed, 256 insertions(+), 3 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index bc92fbd..9f0dbeb 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -484,6 +484,7 @@ ID_REORIENT_IMG = wx.NewId() ID_FLOODFILL_MASK = wx.NewId() ID_REMOVE_MASK_PART = wx.NewId() ID_SELECT_MASK_PART = wx.NewId() +ID_FLOODFILL_SEGMENTATION = wx.NewId() #--------------------------------------------------------- STATE_DEFAULT = 1000 @@ -504,6 +505,7 @@ SLICE_STATE_REORIENT = 3010 SLICE_STATE_MASK_FFILL = 3011 SLICE_STATE_REMOVE_MASK_PARTS = 3012 SLICE_STATE_SELECT_MASK_PARTS = 3013 +SLICE_STATE_FFILL_SEGMENTATION = 3014 VOLUME_STATE_SEED = 2001 # STATE_LINEAR_MEASURE = 3001 @@ -524,6 +526,7 @@ 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) +SLICE_STYLES.append(SLICE_STATE_FFILL_SEGMENTATION) VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, STATE_MEASURE_ANGLE] @@ -535,6 +538,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, SLICE_STATE_MASK_FFILL: 2, SLICE_STATE_REMOVE_MASK_PARTS: 2, SLICE_STATE_SELECT_MASK_PARTS: 2, + SLICE_STATE_FFILL_SEGMENTATION: 2, SLICE_STATE_CROSS: 2, SLICE_STATE_SCROLL: 2, SLICE_STATE_REORIENT: 2, diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index b777961..30ac54b 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -1841,6 +1841,120 @@ class SelectMaskPartsInteractorStyle(DefaultInteractorStyle): self.config.mask = mask +class FFillSegmentationConfig(object): + __metaclass__= utils.Singleton + def __init__(self): + self.dlg_visible = False + self.target = "2D" + self.con_2d = 4 + self.con_3d = 6 + + self.t0 = 0 + self.t1 = 100 + + self.fill_value = 254 + + +class FloodFillSegmentInteractorStyle(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 = FFillSegmentationConfig() + self.dlg_ffill = None + + self._progr_title = _(u"Fill hole") + self._progr_msg = _(u"Filling hole ...") + + self.AddObserver("LeftButtonPressEvent", self.OnFFClick) + + def SetUp(self): + if not self.config.dlg_visible: + self.config.dlg_visible = True + self.dlg_ffill = dialogs.FFillSegmentationOptionsDialog(self.config) + self.dlg_ffill.Show() + + def CleanUp(self): + if (self.dlg_ffill is not None) and (self.config.dlg_visible): + self.config.dlg_visible = False + self.dlg_ffill.Destroy() + self.dlg_ffill = None + + def OnFFClick(self, obj, evt): + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + viewer = self.viewer + iren = viewer.interactor + mouse_x, mouse_y = iren.GetEventPosition() + x, y, z = self.viewer.get_voxel_coord_by_screen_pos(mouse_x, mouse_y, self.picker) + + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + image = self.viewer.slice_.matrix + + if mask[z, y, x] < self.config.t0 or mask[z, y, x] > self.config.t1: + return + + if self.config.target == "3D": + bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') + self.viewer.slice_.do_threshold_to_all_slices() + cp_mask = self.viewer.slice_.current_mask.matrix.copy() + else: + _bstruct = generate_binary_structure(2, CON2D[self.config.con_2d]) + if self.orientation == 'AXIAL': + bstruct = np.zeros((1, 3, 3), dtype='uint8') + bstruct[0] = _bstruct + elif self.orientation == 'CORONAL': + bstruct = np.zeros((3, 1, 3), dtype='uint8') + bstruct[:, 0, :] = _bstruct + elif self.orientation == 'SAGITAL': + bstruct = np.zeros((3, 3, 1), dtype='uint8') + bstruct[:, :, 0] = _bstruct + + if self.config.target == '2D': + floodfill.floodfill_threshold(image, [[x, y, z]], self.config.t0, self.config.t1, self.config.fill_value, bstruct, mask) + b_mask = self.viewer.slice_.buffer_slices[self.orientation].mask + index = self.viewer.slice_.buffer_slices[self.orientation].index + + if self.orientation == 'AXIAL': + p_mask = mask[index,:,:].copy() + elif self.orientation == 'CORONAL': + p_mask = mask[:, index, :].copy() + elif self.orientation == 'SAGITAL': + p_mask = mask[:, :, index].copy() + + self.viewer.slice_.current_mask.save_history(index, self.orientation, p_mask, b_mask) + else: + with futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(floodfill.floodfill_threshold, image, [[x, y, z]], self.config.t0, self.config.t1, self.config.fill_value, bstruct, mask) + + dlg = wx.ProgressDialog(self._progr_title, self._progr_msg, parent=None, style=wx.PD_APP_MODAL) + while not future.done(): + dlg.Pulse() + time.sleep(0.1) + + dlg.Destroy() + + self.viewer.slice_.current_mask.save_history(0, 'VOLUME', self.viewer.slice_.current_mask.matrix.copy(), cp_mask) + + self.viewer.slice_.buffer_slices['AXIAL'].discard_mask() + self.viewer.slice_.buffer_slices['CORONAL'].discard_mask() + self.viewer.slice_.buffer_slices['SAGITAL'].discard_mask() + + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask() + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask() + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() + + self.viewer.slice_.current_mask.was_edited = True + Publisher.sendMessage('Reload actual slice') + + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, @@ -1859,6 +1973,7 @@ def get_style(style): const.SLICE_STATE_MASK_FFILL: FloodFillMaskInteractorStyle, const.SLICE_STATE_REMOVE_MASK_PARTS: RemoveMaskPartsInteractorStyle, const.SLICE_STATE_SELECT_MASK_PARTS: SelectMaskPartsInteractorStyle, + const.SLICE_STATE_FFILL_SEGMENTATION: FloodFillSegmentInteractorStyle, } return STYLES[style] diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 0fc4dd4..81183e4 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -2031,3 +2031,124 @@ class SelectPartsOptionsDialog(wx.Dialog): Publisher.sendMessage('Disable style', const.SLICE_STATE_SELECT_MASK_PARTS) evt.Skip() self.Destroy() + + +class FFillSegmentationOptionsDialog(wx.Dialog): + def __init__(self, config): + pre = wx.PreDialog() + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Floodfill Segmentation"), style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) + self.PostCreate(pre) + + self.config = config + + self._init_gui() + + def _init_gui(self): + """ + Create the widgets. + """ + import project as prj + # Target + self.target_2d = wx.RadioButton(self, -1, _(u"2D - Actual slice"), style=wx.RB_GROUP) + self.target_3d = wx.RadioButton(self, -1, _(u"3D - All slices")) + + if self.config.target == "2D": + self.target_2d.SetValue(1) + else: + self.target_3d.SetValue(1) + + # Connectivity 2D + self.conect2D_4 = wx.RadioButton(self, -1, "4", style=wx.RB_GROUP) + self.conect2D_8 = wx.RadioButton(self, -1, "8") + + if self.config.con_2d == 8: + self.conect2D_8.SetValue(1) + else: + self.conect2D_4.SetValue(1) + self.config.con_2d = 4 + + # 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) + + project = prj.Project() + bound_min, bound_max = project.threshold_range + colour = [i*255 for i in const.MASK_COLOUR[0]] + colour.append(100) + self.threshold = grad.GradientCtrl(self, -1, int(bound_min), + int(bound_max), self.config.t0, + self.config.t1, colour) + + # Sizer + sizer = wx.GridBagSizer(15, 6) + sizer.AddStretchSpacer((0, 0)) + + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), (1, 0), (1, 6), flag=wx.LEFT, border=7) + sizer.Add(self.target_2d, (2, 0), (1, 6), flag=wx.LEFT, border=9) + sizer.Add(self.target_3d, (3, 0), (1, 6), flag=wx.LEFT, border=9) + + sizer.AddStretchSpacer((4, 0)) + + sizer.Add(wx.StaticText(self, -1, _(u"2D Connectivity")), (5, 0), (1, 6), flag=wx.LEFT, border=9) + sizer.Add(self.conect2D_4, (6, 0), flag=wx.LEFT, border=9) + sizer.Add(self.conect2D_8, (6, 1), flag=wx.LEFT, border=9) + + sizer.AddStretchSpacer((7, 0)) + + sizer.Add(wx.StaticText(self, -1, _(u"3D Connectivity")), (8, 0), (1, 6), flag=wx.LEFT, border=9) + sizer.Add(self.conect3D_6, (9, 0), flag=wx.LEFT, border=9) + sizer.Add(self.conect3D_18, (9, 1), flag=wx.LEFT, border=9) + sizer.Add(self.conect3D_26, (9, 2), flag=wx.LEFT, border=9) + sizer.AddStretchSpacer((10, 0)) + + sizer.Add(wx.StaticText(self, -1, _(u"Threshold")), (11, 0), (1, 6), flag=wx.LEFT, border=9) + sizer.Add(self.threshold, (12, 0), (1, 6), flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=9) + sizer.AddStretchSpacer((13, 0)) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() + + self.Bind(wx.EVT_RADIOBUTTON, self.OnSetRadio) + self.Bind(grad.EVT_THRESHOLD_CHANGING, self.OnSlideChanged, self.threshold) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + def OnSetRadio(self, evt): + # Target + if self.target_2d.GetValue(): + self.config.target = "2D" + else: + self.config.target = "3D" + + # 2D + if self.conect2D_4.GetValue(): + self.config.con_2d = 4 + elif self.conect2D_8.GetValue(): + self.config.con_2d = 8 + + # 3D + 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 OnSlideChanged(self, evt): + self.config.t0 = self.threshold.GetMinValue() + self.config.t1 = self.threshold.GetMaxValue() + print self.config.t0, self.config.t1 + + def OnClose(self, evt): + if self.config.dlg_visible: + Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL) + evt.Skip() + self.Destroy() diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 3089316..6508c9a 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -454,6 +454,9 @@ class Frame(wx.Frame): elif id == const.ID_SELECT_MASK_PART: self.OnSelectMaskParts() + elif id == const.ID_FLOODFILL_SEGMENTATION: + self.OnFFillSegmentation() + elif id == const.ID_VIEW_INTERPOLATED: st = self.actived_interpolated_slices.IsChecked(const.ID_VIEW_INTERPOLATED) if st: @@ -593,6 +596,9 @@ class Frame(wx.Frame): def OnSelectMaskParts(self): Publisher.sendMessage('Enable style', const.SLICE_STATE_SELECT_MASK_PARTS) + def OnFFillSegmentation(self): + Publisher.sendMessage('Enable style', const.SLICE_STATE_FFILL_SEGMENTATION) + def OnInterpolatedSlices(self, status): Publisher.sendMessage('Set interpolated slices', status) @@ -618,7 +624,8 @@ class MenuBar(wx.MenuBar): const.ID_REORIENT_IMG, const.ID_FLOODFILL_MASK, const.ID_REMOVE_MASK_PART, - const.ID_SELECT_MASK_PART,] + const.ID_SELECT_MASK_PART, + const.ID_FLOODFILL_SEGMENTATION,] self.__init_items() self.__bind_events() @@ -741,6 +748,13 @@ class MenuBar(wx.MenuBar): tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) + # Segmentation Menu + segmentation_menu = wx.Menu() + self.ffill_segmentation = segmentation_menu.Append(const.ID_FLOODFILL_SEGMENTATION, _(u"Floodfill")) + self.ffill_segmentation.Enable(False) + + tools_menu.AppendMenu(-1, _("Segmentation"), segmentation_menu) + # Image menu image_menu = wx.Menu() reorient_menu = image_menu.Append(const.ID_REORIENT_IMG, _(u'Reorient image\tCtrl+Shift+R')) @@ -758,8 +772,6 @@ class MenuBar(wx.MenuBar): self.view_menu.Check(const.ID_VIEW_INTERPOLATED, v) self.actived_interpolated_slices = self.view_menu - - #view_tool_menu = wx.Menu() #app = view_tool_menu.Append diff --git a/invesalius/gui/widgets/gradient.py b/invesalius/gui/widgets/gradient.py index e93dd95..9f4fc30 100755 --- a/invesalius/gui/widgets/gradient.py +++ b/invesalius/gui/widgets/gradient.py @@ -493,6 +493,7 @@ class GradientCtrl(wx.Panel): return self.minimun def _GenerateEvent(self, event): + print "GEN" if event == myEVT_THRESHOLD_CHANGING: self.changed = True elif event == myEVT_THRESHOLD_CHANGED : -- libgit2 0.21.2