From 16a1bfd7153c0613dfee307397d5a902edf0da65 Mon Sep 17 00:00:00 2001 From: Thiago Franco de Moraes Date: Tue, 13 Sep 2016 10:38:10 -0300 Subject: [PATCH] Floodfill segmentation (2D and 3D) --- invesalius/constants.py | 4 ++++ invesalius/data/styles.py | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/gui/dialogs.py | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/gui/frame.py | 18 +++++++++++++++--- 4 files changed, 398 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..980d607 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -76,6 +76,16 @@ def get_LUT_value(data, window, level): data.shape = shape return data +def get_LUT_value_255(data, window, level): + shape = data.shape + data_ = data.ravel() + data = np.piecewise(data_, + [data_ <= (level - 0.5 - (window-1)/2), + data_ > (level - 0.5 + (window-1)/2)], + [0, 255, lambda data_: ((data_ - (level - 0.5))/(window-1) + 0.5)*(255)]) + data.shape = shape + return data + class BaseImageInteractorStyle(vtk.vtkInteractorStyleImage): def __init__(self, viewer): @@ -1841,6 +1851,200 @@ 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 = None + self.t1 = None + + self.fill_value = 254 + + self.method = 'threshold' + + self.dev_min = 25 + self.dev_max = 25 + + self.use_ww_wl = True + + +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"Floodfill segmentation") + self._progr_msg = _(u"Segmenting ...") + + self.AddObserver("LeftButtonPressEvent", self.OnFFClick) + + def SetUp(self): + if not self.config.dlg_visible: + + if self.config.t0 is None: + image = self.viewer.slice_.matrix + _min, _max = image.min(), image.max() + + self.config.t0 = int(_min + (3.0/4.0) * (_max - _min)) + self.config.t1 = int(_max) + + 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 + + if self.config.target == "3D": + self.do_3d_seg() + with futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(self.do_3d_seg) + + 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() + + else: + self.do_2d_seg() + + 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 do_2d_seg(self): + viewer = self.viewer + iren = viewer.interactor + mouse_x, mouse_y = iren.GetEventPosition() + x, y = self.viewer.get_slice_pixel_coord_by_screen_pos(mouse_x, mouse_y, self.picker) + + mask = self.viewer.slice_.buffer_slices[self.orientation].mask.copy() + image = self.viewer.slice_.buffer_slices[self.orientation].image + + if self.config.method == 'threshold': + v = image[y, x] + t0 = self.config.t0 + t1 = self.config.t1 + + elif self.config.method == 'dynamic': + if self.config.use_ww_wl: + print "Using WW&WL" + ww = self.viewer.slice_.window_width + wl = self.viewer.slice_.window_level + image = get_LUT_value_255(image, ww, wl) + + v = image[y, x] + + t0 = v - self.config.dev_min + t1 = v + self.config.dev_max + + if image[y, x] < t0 or image[y, x] > t1: + return + + dy, dx = image.shape + image = image.reshape((1, dy, dx)) + mask = mask.reshape((1, dy, dx)) + + out_mask = np.zeros_like(mask) + + bstruct = np.array(generate_binary_structure(2, CON2D[self.config.con_2d]), dtype='uint8') + bstruct = bstruct.reshape((1, 3, 3)) + + floodfill.floodfill_threshold(image, [[x, y, 0]], t0, t1, 1, bstruct, out_mask) + + mask[out_mask.astype('bool')] = self.config.fill_value + + index = self.viewer.slice_.buffer_slices[self.orientation].index + b_mask = self.viewer.slice_.buffer_slices[self.orientation].mask + vol_mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + + if self.orientation == 'AXIAL': + vol_mask[index, :, :] = mask + elif self.orientation == 'CORONAL': + vol_mask[:, index, :] = mask + elif self.orientation == 'SAGITAL': + vol_mask[:, :, index] = mask + + self.viewer.slice_.current_mask.save_history(index, self.orientation, mask, b_mask) + + def do_3d_seg(self): + 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 self.config.method == 'threshold': + v = image[z, y, x] + t0 = self.config.t0 + t1 = self.config.t1 + + elif self.config.method == 'dynamic': + if self.config.use_ww_wl: + print "Using WW&WL" + ww = self.viewer.slice_.window_width + wl = self.viewer.slice_.window_level + image = get_LUT_value_255(image, ww, wl) + + v = image[z, y, x] + + t0 = v - self.config.dev_min + t1 = v + self.config.dev_max + + if image[z, y, x] < t0 or image[z, y, x] > t1: + return + + out_mask = np.zeros_like(mask) + + 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() + + with futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(floodfill.floodfill_threshold, image, [[x, y, z]], t0, t1, 1, bstruct, out_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() + + mask[out_mask.astype('bool')] = self.config.fill_value + + self.viewer.slice_.current_mask.save_history(0, 'VOLUME', self.viewer.slice_.current_mask.matrix.copy(), cp_mask) + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, @@ -1859,6 +2063,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..565381e 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -2031,3 +2031,177 @@ 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) + self.threshold.SetMinSize((250, -1)) + + self.method_threshold = wx.RadioButton(self, -1, _(u"Threshold"), style=wx.RB_GROUP) + self.method_dynamic = wx.RadioButton(self, -1, _(u"Dynamic")) + + if self.config.method == 'dynamic': + self.method_dynamic.SetValue(1) + else: + self.method_threshold.SetValue(1) + self.config.method = 'threshold' + + self.use_ww_wl = wx.CheckBox(self, -1, _(u"Use WW\&WL")) + self.use_ww_wl.SetValue(self.config.use_ww_wl) + + self.deviation_min = wx.SpinCtrl(self, -1, value='%d' % self.config.dev_min, min=0, max=10000) + self.deviation_max = wx.SpinCtrl(self, -1, value='%d' % self.config.dev_max, min=0, max=10000) + + # Sizer + sizer = wx.BoxSizer(wx.VERTICAL) + + sizer.AddSpacer(7) + + sizer.Add(wx.StaticText(self, -1, _(u"Parameters")), flag=wx.LEFT, border=7) + sizer.AddSpacer(5) + sizer.Add(self.target_2d, flag=wx.LEFT, border=9) + sizer.Add(self.target_3d, flag=wx.LEFT, border=9) + + sizer.AddSpacer(7) + + sizer.Add(wx.StaticText(self, -1, _(u"2D Connectivity")), flag=wx.LEFT, border=9) + sizer.AddSpacer(5) + sizer_2d = wx.BoxSizer(wx.HORIZONTAL) + sizer_2d.Add(self.conect2D_4, flag=wx.LEFT, border=11) + sizer_2d.Add(self.conect2D_8, flag=wx.LEFT, border=11) + sizer.Add(sizer_2d) + + sizer.AddSpacer(7) + + sizer.Add(wx.StaticText(self, -1, _(u"3D Connectivity")), flag=wx.LEFT, border=9) + sizer.AddSpacer(5) + sizer_3d = wx.BoxSizer(wx.HORIZONTAL) + sizer_3d.Add(self.conect3D_6, flag=wx.LEFT, border=11) + sizer_3d.Add(self.conect3D_18, flag=wx.LEFT, border=11) + sizer_3d.Add(self.conect3D_26, flag=wx.LEFT, border=11) + sizer.Add(sizer_3d) + + sizer.AddSpacer(7) + + sizer.Add(wx.StaticText(self, -1, _(u"Method")), flag=wx.LEFT, border=9) + sizer.AddSpacer(5) + sizer.Add(self.method_threshold, flag=wx.LEFT, border=11) + sizer.AddSpacer(5) + sizer.Add(self.threshold, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=13) + sizer.AddSpacer(5) + sizer.Add(self.method_dynamic, flag=wx.LEFT, border=11) + sizer.AddSpacer(5) + sizer.Add(self.use_ww_wl, flag=wx.LEFT, border=13) + sizer.AddSpacer(5) + sizer.Add(self.deviation_min, flag=wx.LEFT, border=13) + sizer.Add(self.deviation_max, flag=wx.LEFT, border=13) + + sizer.AddSpacer(7) + + 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(grad.EVT_THRESHOLD_CHANGED, self.OnSlideChanged, self.threshold) + self.use_ww_wl.Bind(wx.EVT_CHECKBOX, self.OnSetUseWWWL) + self.deviation_min.Bind(wx.EVT_SPINCTRL, self.OnSetDeviation) + self.deviation_max.Bind(wx.EVT_SPINCTRL, self.OnSetDeviation) + 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 + + # Method + if self.method_threshold.GetValue(): + self.config.method = 'threshold' + else: + self.config.method = 'dynamic' + + def OnSlideChanged(self, evt): + self.config.t0 = int(self.threshold.GetMinValue()) + self.config.t1 = int(self.threshold.GetMaxValue()) + print self.config.t0, self.config.t1 + + def OnSetUseWWWL(self, evt): + self.config.use_ww_wl = self.use_ww_wl.GetValue() + + def OnSetDeviation(self, evt): + self.config.dev_max = self.deviation_max.GetValue() + self.config.dev_min = self.deviation_min.GetValue() + + 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 -- libgit2 0.21.2