diff --git a/icons/configuration.png b/icons/configuration.png new file mode 100644 index 0000000..a857ddd Binary files /dev/null and b/icons/configuration.png differ diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index ca93b3e..c594e0b 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -18,7 +18,9 @@ #-------------------------------------------------------------------------- import os +import multiprocessing import tempfile +import time import vtk import wx @@ -27,13 +29,17 @@ from wx.lib.pubsub import pub as Publisher import constants as const import converters +import cursor_actors as ca import numpy as np from scipy import ndimage from scipy.misc import imsave +from scipy.ndimage import watershed_ift, generate_binary_structure from skimage.morphology import watershed from skimage import filter +import utils + ORIENTATIONS = { "AXIAL": const.AXIAL, "CORONAL": const.CORONAL, @@ -734,6 +740,86 @@ class EditorInteractorStyle(DefaultInteractorStyle): return x, y, z +class WatershedProgressWindow(wx.Frame): + def __init__(self, process, parent=None): + wx.Frame.__init__(self, parent, -1) + self.process = process + self._build_gui() + self._bind_wx_events() + self.timer = wx.Timer(self) + self.timer.Start(1000) + + def _build_gui(self): + self.gauge = wx.Gauge(self, -1, 100) + self.btn_cancel = wx.Button(self, wx.ID_CANCEL) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(wx.StaticText(self, -1, _("Applying watershed"))) + sizer.Add(self.gauge, 0, wx.EXPAND) + sizer.Add(self.btn_cancel, 0, wx.ALIGN_LEFT) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() + + def __del__(self): + self.timer.Stop() + + def _bind_wx_events(self): + self.Bind(wx.EVT_TIMER, self.TimeHandler) + self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) + + def on_cancel(self, evt): + self.timer.Stop() + self.process.terminate() + + def TimeHandler(self, evt): + self.gauge.Pulse() + + +class WatershedConfig(object): + __metaclass__= utils.Singleton + def __init__(self): + self.algorithm = "Watershed" + self.con_2d = 4 + self.con_3d = 6 + self.mg_size = 3 + self.use_ww_wl = True + self.operation = BRUSH_FOREGROUND + self.cursor_type = const.BRUSH_CIRCLE + self.cursor_size = const.BRUSH_SIZE + + Publisher.subscribe(self.set_operation, 'Set watershed operation') + Publisher.subscribe(self.set_use_ww_wl, 'Set use ww wl') + + Publisher.subscribe(self.set_algorithm, "Set watershed algorithm") + Publisher.subscribe(self.set_2dcon, "Set watershed 2d con") + Publisher.subscribe(self.set_3dcon, "Set watershed 3d con") + Publisher.subscribe(self.set_gaussian_size, "Set watershed gaussian size") + + def set_operation(self, pubsub_evt): + self.operation = WATERSHED_OPERATIONS[pubsub_evt.data] + + def set_use_ww_wl(self, pubsub_evt): + self.use_ww_wl = pubsub_evt.data + + def set_algorithm(self, pubsub_evt): + self.algorithm = pubsub_evt.data + + def set_2dcon(self, pubsub_evt): + self.con_2d = pubsub_evt.data + + def set_3dcon(self, pubsub_evt): + self.con_3d = pubsub_evt.data + + def set_gaussian_size(self, pubsub_evt): + self.mg_size = pubsub_evt.data + +WALGORITHM = {"Watershed": watershed, + "Watershed IFT": watershed_ift} +CON2D = {4: 1, 8: 2} +CON3D = {6: 1, 18: 2, 26: 3} + class WaterShedInteractorStyle(DefaultInteractorStyle): def __init__(self, viewer): DefaultInteractorStyle.__init__(self, viewer) @@ -742,9 +828,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): self.orientation = self.viewer.orientation self.matrix = None - self.operation = BRUSH_FOREGROUND - - self.mg_size = 3 + self.config = WatershedConfig() self.picker = vtk.vtkWorldPointPicker() @@ -761,7 +845,10 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): self.AddObserver("MouseMoveEvent", self.OnBrushMove) Publisher.subscribe(self.expand_watershed, 'Expand watershed to 3D ' + self.orientation) - Publisher.subscribe(self.set_operation, 'Set watershed operation') + Publisher.subscribe(self.set_bsize, 'Set watershed brush size') + Publisher.subscribe(self.set_bformat, 'Set watershed brush format') + + self._set_cursor() def SetUp(self): mask = self.viewer.slice_.current_mask.matrix @@ -772,7 +859,8 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): def CleanUp(self): #self._remove_mask() Publisher.unsubscribe(self.expand_watershed, 'Expand watershed to 3D ' + self.orientation) - Publisher.unsubscribe(self.set_operation, 'Set watershed operation') + Publisher.unsubscribe(self.set_bformat, 'Set watershed brush format') + Publisher.unsubscribe(self.set_bsize, 'Set watershed brush size') self.RemoveAllObservers() self.viewer.slice_.to_show_aux = '' self.viewer.OnScrollBar() @@ -791,8 +879,33 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): os.remove(self.temp_file) print "deleting", self.temp_file - def set_operation(self, pubsub_evt): - self.operation = WATERSHED_OPERATIONS[pubsub_evt.data] + def _set_cursor(self): + if self.config.cursor_type == const.BRUSH_SQUARE: + cursor = ca.CursorRectangle() + elif self.config.cursor_type == const.BRUSH_CIRCLE: + cursor = ca.CursorCircle() + + cursor.SetOrientation(self.orientation) + n = self.viewer.slice_data.number + coordinates = {"SAGITAL": [n, 0, 0], + "CORONAL": [0, n, 0], + "AXIAL": [0, 0, n]} + cursor.SetPosition(coordinates[self.orientation]) + spacing = self.viewer.slice_.spacing + cursor.SetSpacing(spacing) + cursor.SetColour(self.viewer._brush_cursor_colour) + cursor.SetSize(self.config.cursor_size) + self.viewer.slice_data.SetCursor(cursor) + self.viewer.interactor.Render() + + def set_bsize(self, pubsub_evt): + size = pubsub_evt.data + self.config.cursor_size = size + self.viewer.slice_data.cursor.SetSize(size) + + def set_bformat(self, pubsub_evt): + self.config.cursor_type = pubsub_evt.data + self._set_cursor() def OnEnterInteractor(self, obj, evt): if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): @@ -869,7 +982,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): if position < 0: position = viewer.calculate_matrix_position(coord) - operation = self.operation + operation = self.config.operation if operation == BRUSH_FOREGROUND: if iren.GetControlKey(): @@ -937,7 +1050,7 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): if position < 0: position = viewer.calculate_matrix_position(coord) - operation = self.operation + operation = self.config.operation if operation == BRUSH_FOREGROUND: if iren.GetControlKey(): @@ -991,8 +1104,35 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): wl = self.viewer.slice_.window_level if BRUSH_BACKGROUND in markers and BRUSH_FOREGROUND in markers: - tmp_image = ndimage.morphological_gradient(get_LUT_value(image, ww, wl).astype('uint16'), self.mg_size) - tmp_mask = watershed(tmp_image, markers) + #w_algorithm = WALGORITHM[self.config.algorithm] + bstruct = generate_binary_structure(2, CON2D[self.config.con_2d]) + if self.config.use_ww_wl: + if self.config.algorithm == 'Watershed': + tmp_image = ndimage.morphological_gradient( + get_LUT_value(image, ww, wl).astype('uint16'), + self.config.mg_size) + tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + else: + #tmp_image = ndimage.gaussian_filter(get_LUT_value(image, ww, wl).astype('uint16'), self.config.mg_size) + #tmp_image = ndimage.morphological_gradient( + #get_LUT_value(image, ww, wl).astype('uint16'), + #self.config.mg_size) + tmp_image = get_LUT_value(image, ww, wl).astype('uint16') + #markers[markers == 2] = -1 + tmp_mask = watershed_ift(tmp_image, markers.astype('int16'), bstruct) + #markers[markers == -1] = 2 + #tmp_mask[tmp_mask == -1] = 2 + + else: + if self.config.algorithm == 'Watershed': + tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) + tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + else: + #tmp_image = (image - image.min()).astype('uint16') + #tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) + #tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) + tmp_image = image - image.min().astype('uint16') + tmp_mask = watershed_ift(tmp_image, markers.astype('int16'), bstruct) if self.viewer.overwrite_mask: mask[:] = 0 @@ -1098,8 +1238,59 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): ww = self.viewer.slice_.window_width wl = self.viewer.slice_.window_level if BRUSH_BACKGROUND in markers and BRUSH_FOREGROUND in markers: - tmp_image = ndimage.morphological_gradient(get_LUT_value(image, ww, wl).astype('uint16'), self.mg_size) - tmp_mask = watershed(tmp_image, markers) + #w_algorithm = WALGORITHM[self.config.algorithm] + bstruct = generate_binary_structure(3, CON3D[self.config.con_3d]) + tfile = tempfile.mktemp() + tmp_mask = np.memmap(tfile, shape=mask.shape, dtype=mask.dtype, + mode='w+') + q = multiprocessing.Queue() + p = multiprocessing.Process(target=do_watershed, args=(image, + markers, tmp_mask, bstruct, + self.config.algorithm, + self.config.mg_size, + self.config.use_ww_wl, wl, ww, q)) + + wp = WatershedProgressWindow(p) + wp.Center(wx.BOTH) + wp.Show() + wp.MakeModal() + + p.start() + + while q.empty() and p.is_alive(): + time.sleep(0.5) + wx.Yield() + + wp.MakeModal(False) + wp.Destroy() + del wp + + if q.empty(): + return + #do_watershed(image, markers, tmp_mask, bstruct, self.config.algorithm, + #self.config.mg_size, self.config.use_ww_wl, wl, ww) + #if self.config.use_ww_wl: + #if self.config.algorithm == 'Watershed': + #tmp_image = ndimage.morphological_gradient( + #get_LUT_value(image, ww, wl).astype('uint16'), + #self.config.mg_size) + #tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + #else: + #tmp_image = get_LUT_value(image, ww, wl).astype('uint16') + ##tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) + ##tmp_image = ndimage.morphological_gradient( + ##get_LUT_value(image, ww, wl).astype('uint16'), + ##self.config.mg_size) + #tmp_mask = watershed_ift(tmp_image, markers.astype('int16'), bstruct) + #else: + #if self.config.algorithm == 'Watershed': + #tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) + #tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + #else: + #tmp_image = (image - image.min()).astype('uint16') + ##tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) + ##tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) + #tmp_mask = watershed_ift(tmp_image, markers.astype('int8'), bstruct) if self.viewer.overwrite_mask: mask[:] = 0 @@ -1118,6 +1309,34 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): Publisher.sendMessage('Reload actual slice') +def do_watershed(image, markers, mask, bstruct, algorithm, mg_size, use_ww_wl, wl, ww, q): + if use_ww_wl: + if algorithm == 'Watershed': + tmp_image = ndimage.morphological_gradient( + get_LUT_value(image, ww, wl).astype('uint16'), + mg_size) + tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + else: + tmp_image = get_LUT_value(image, ww, wl).astype('uint16') + #tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) + #tmp_image = ndimage.morphological_gradient( + #get_LUT_value(image, ww, wl).astype('uint16'), + #self.config.mg_size) + tmp_mask = watershed_ift(tmp_image, markers.astype('int16'), bstruct) + else: + if algorithm == 'Watershed': + tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), mg_size) + tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + else: + tmp_image = (image - image.min()).astype('uint16') + #tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) + #tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) + tmp_mask = watershed_ift(tmp_image, markers.astype('int8'), bstruct) + mask[:] = tmp_mask + q.put(1) + + + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index c6d7271..949d35b 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1384,3 +1384,88 @@ class ClutImagedataDialog(wx.Dialog): super(wx.Dialog, self).Show(show) if gen_evt: self.clut_widget._generate_event() + + +class WatershedOptions(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + self.algorithms = ("Watershed", "Watershed IFT") + self.con2d_choices = (4, 8) + self.con3d_choices = (6, 18, 26) + + self._init_gui() + self._bind_events() + + def _init_gui(self): + self.choice_algorithm = wx.RadioBox(self, -1, "Algorithm", + choices=("Watershed", "Watershed IFT"), + style=wx.NO_BORDER | wx.HORIZONTAL) + + self.choice_2dcon = wx.RadioBox(self, -1, "2D", + choices=[str(i) for i in self.con2d_choices], + style=wx.NO_BORDER | wx.HORIZONTAL) + + self.choice_3dcon = wx.RadioBox(self, -1, "3D", + choices=[str(i) for i in self.con3d_choices], + style=wx.NO_BORDER | wx.HORIZONTAL) + + self.gaussian_size = wx.SpinCtrl(self, -1, "", min=1, max=10) + + box_sizer = wx.StaticBoxSizer(wx.StaticBox(self, -1, "Conectivity"), wx.VERTICAL) + box_sizer.Add(self.choice_2dcon, 0, wx.ALIGN_CENTER_VERTICAL,2) + box_sizer.Add(self.choice_3dcon, 0, wx.ALIGN_CENTER_VERTICAL,2) + + g_sizer = wx.BoxSizer(wx.HORIZONTAL) + g_sizer.Add(wx.StaticText(self, -1, "Gaussian size"), 0, wx.ALIGN_RIGHT | wx.ALL, 5) + g_sizer.Add(self.gaussian_size, 0, wx.ALIGN_LEFT | wx.ALL, 5) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.choice_algorithm, 0, wx.ALIGN_CENTER_VERTICAL,2) + sizer.Add(box_sizer, 1, wx.EXPAND,2) + sizer.Add(g_sizer, 0, wx.ALIGN_LEFT, 2) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() + + def _bind_events(self): + self.choice_algorithm.Bind(wx.EVT_RADIOBOX, self.OnSetAlgorithm) + self.gaussian_size.Bind(wx.EVT_SPINCTRL, self.OnSetGaussianSize) + self.choice_2dcon.Bind(wx.EVT_RADIOBOX, self.OnSetCon2D) + self.choice_3dcon.Bind(wx.EVT_RADIOBOX, self.OnSetCon3D) + + def OnSetAlgorithm(self, evt): + v = self.algorithms[evt.GetInt()] + Publisher.sendMessage("Set watershed algorithm", v) + + def OnSetGaussianSize(self, evt): + v = self.gaussian_size.GetValue() + Publisher.sendMessage("Set watershed gaussian size", v) + + def OnSetCon2D(self, evt): + v = self.con2d_choices[evt.GetInt()] + Publisher.sendMessage("Set watershed 2d con", v) + + def OnSetCon3D(self, evt): + v = self.con3d_choices[evt.GetInt()] + Publisher.sendMessage("Set watershed 3d con", v) + + +class WatershedOptionsDialog(wx.Dialog): + def __init__(self): + pre = wx.PreDialog() + pre.Create(wx.GetApp().GetTopWindow(), -1, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) + self.PostCreate(pre) + + self._init_gui() + + def _init_gui(self): + wop = WatershedOptions(self) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(wop, 0, wx.EXPAND) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() diff --git a/invesalius/gui/task_slice.py b/invesalius/gui/task_slice.py index d77570e..a44ec1d 100644 --- a/invesalius/gui/task_slice.py +++ b/invesalius/gui/task_slice.py @@ -720,7 +720,7 @@ class WatershedTool(EditionTools): self.SetBackgroundColour(default_colour) ## LINE 1 - text1 = wx.StaticText(self, -1, _("Choose brush type and size:")) + text1 = wx.StaticText(self, -1, _("Choose brush type, size or operation:")) ## LINE 2 menu = wx.Menu() @@ -773,17 +773,28 @@ class WatershedTool(EditionTools): # LINE 5 check_box = wx.CheckBox(self, -1, _("Overwrite mask")) + ww_wl_cbox = wx.CheckBox(self, -1, _("Use WW&WL")) + ww_wl_cbox.SetValue(True) self.check_box = check_box + self.ww_wl_cbox = ww_wl_cbox # Line 6 + bmp = wx.Bitmap("../icons/configuration.png", wx.BITMAP_TYPE_PNG) + self.btn_wconfig = wx.BitmapButton(self, -1, bitmap=bmp, + size=(bmp.GetWidth()+10, bmp.GetHeight()+10)) self.btn_exp_watershed = wx.Button(self, -1, _('Expand watershed to 3D')) + sizer_btns = wx.BoxSizer(wx.HORIZONTAL) + sizer_btns.Add(self.btn_wconfig, 0, wx.ALIGN_LEFT | wx.LEFT | wx.TOP | wx.DOWN, 5) + sizer_btns.Add(self.btn_exp_watershed, 0, wx.GROW|wx.EXPAND| wx.ALL, 5) + # Add lines into main sizer sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(text1, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5) sizer.Add(line2, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5) sizer.Add(check_box, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5) - sizer.Add(self.btn_exp_watershed, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5) + sizer.Add(ww_wl_cbox, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5) + sizer.Add(sizer_btns, 0, wx.EXPAND) sizer.Fit(self) self.SetSizer(sizer) @@ -797,7 +808,9 @@ class WatershedTool(EditionTools): self.Bind(wx.EVT_MENU, self.OnMenu) self.combo_brush_op.Bind(wx.EVT_COMBOBOX, self.OnComboBrushOp) self.check_box.Bind(wx.EVT_CHECKBOX, self.OnCheckOverwriteMask) + self.ww_wl_cbox.Bind(wx.EVT_CHECKBOX, self.OnCheckWWWL) self.btn_exp_watershed.Bind(wx.EVT_BUTTON, self.OnExpandWatershed) + self.btn_wconfig.Bind(wx.EVT_BUTTON, self.OnConfig) def ChangeMaskColour(self, pubsub_evt): colour = pubsub_evt.data @@ -834,14 +847,14 @@ class WatershedTool(EditionTools): self.btn_brush_format.SetBitmap(bitmap[evt.GetId()]) - Publisher.sendMessage('Set brush format', brush[evt.GetId()]) + Publisher.sendMessage('Set watershed brush format', brush[evt.GetId()]) def OnBrushSize(self, evt): """ """ # FIXME: Using wx.EVT_SPINCTRL in MacOS it doesnt capture changes only # in the text ctrl - so we are capturing only changes on text # Strangelly this is being called twice - Publisher.sendMessage('Set edition brush size',self.spin.GetValue()) + Publisher.sendMessage('Set watershed brush size',self.spin.GetValue()) def OnComboBrushOp(self, evt): brush_op = self.combo_brush_op.GetValue() @@ -851,5 +864,12 @@ class WatershedTool(EditionTools): value = self.check_box.GetValue() Publisher.sendMessage('Set overwrite mask', value) + def OnCheckWWWL(self, evt): + value = self.ww_wl_cbox.GetValue() + Publisher.sendMessage('Set use ww wl', value) + + def OnConfig(self, evt): + dlg.WatershedOptionsDialog().Show() + def OnExpandWatershed(self, evt): Publisher.sendMessage('Expand watershed to 3D AXIAL') -- libgit2 0.21.2