diff --git a/invesalius/constants.py b/invesalius/constants.py index 3455ae4..f6db534 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -485,6 +485,7 @@ STATE_MEASURE_ANGLE = 1008 SLICE_STATE_CROSS = 3006 SLICE_STATE_SCROLL = 3007 SLICE_STATE_EDITOR = 3008 +SLICE_STATE_WATERSHED = 3009 VOLUME_STATE_SEED = 2001 #STATE_LINEAR_MEASURE = 3001 @@ -500,6 +501,7 @@ TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL] SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES SLICE_STYLES.append(STATE_DEFAULT) SLICE_STYLES.append(SLICE_STATE_EDITOR) +SLICE_STYLES.append(SLICE_STATE_WATERSHED) VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, STATE_MEASURE_ANGLE] @@ -507,6 +509,7 @@ VOLUME_STYLES.append(STATE_DEFAULT) STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, + SLICE_STATE_WATERSHED: 1, SLICE_STATE_CROSS: 2, SLICE_STATE_SCROLL: 2, STATE_ANNOTATE: 2, diff --git a/invesalius/control.py b/invesalius/control.py index 4e526b0..ff4671c 100644 --- a/invesalius/control.py +++ b/invesalius/control.py @@ -127,6 +127,11 @@ class Controller(): answer = dialog.SaveChangesDialog2(filename) if answer: self.ShowDialogSaveProject() + self.CloseProject() + #Publisher.sendMessage("Enable state project", False) + Publisher.sendMessage('Set project name') + Publisher.sendMessage("Stop Config Recording") + Publisher.sendMessage("Set slice interaction style", const.STATE_DEFAULT) # Import project dirpath = dialog.ShowImportDirDialog() if dirpath and not os.listdir(dirpath): diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index 4816b96..4475efd 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -84,6 +84,10 @@ class Slice(object): self.blend_filter = None self.histogram = None self._matrix = None + self.aux_matrices = {} + self.state = const.STATE_DEFAULT + + self.to_show_aux = '' self._type_projection = const.PROJECTION_NORMAL self.n_border = const.PROJECTION_BORDER_SIZE @@ -107,6 +111,7 @@ class Slice(object): self.from_ = OTHER self.__bind_events() + self.opacity = 0.8 @property def matrix(self): @@ -187,6 +192,11 @@ class Slice(object): elif orientation == 'SAGITAL': return shape[2] - 1 + def discard_all_buffers(self): + for buffer_ in self.buffer_slices.values(): + buffer_.discard_vtk_mask() + buffer_.discard_mask() + def OnRemoveMasks(self, pubsub_evt): selected_items = pubsub_evt.data proj = Project() @@ -228,6 +238,7 @@ class Slice(object): if (state in const.SLICE_STYLES): new_state = self.interaction_style.AddState(state) Publisher.sendMessage('Set slice interaction style', new_state) + self.state = state def OnDisableStyle(self, pubsub_evt): state = pubsub_evt.data @@ -237,6 +248,7 @@ class Slice(object): if (state == const.SLICE_STATE_EDITOR): Publisher.sendMessage('Set interactor default cursor') + self.state = new_state def OnCloseProject(self, pubsub_evt): self.CloseProject() @@ -249,9 +261,18 @@ class Slice(object): os.remove(f) self.current_mask = None + for name in self.aux_matrices: + m = self.aux_matrices[name] + f = m.filename + m._mmap.close() + m = None + os.remove(f) + self.aux_matrices = {} + self.values = None self.nodes = None self.from_= OTHER + self.state = const.STATE_DEFAULT self.number_of_colours = 256 self.saturation_range = (0, 0) @@ -380,6 +401,12 @@ class Slice(object): value = False Publisher.sendMessage('Show mask', (index, value)) + def create_temp_mask(self): + temp_file = tempfile.mktemp() + shape = self.matrix.shape + matrix = numpy.memmap(temp_file, mode='w+', dtype='uint8', shape=shape) + return temp_file, matrix + def edit_mask_pixel(self, operation, index, position, radius, orientation): mask = self.buffer_slices[orientation].mask image = self.buffer_slices[orientation].image @@ -479,7 +506,7 @@ class Slice(object): print "Do not getting from buffer" n_mask = self.get_mask_slice(orientation, slice_number) mask = converters.to_vtk(n_mask, self.spacing, slice_number, orientation) - mask = self.do_colour_mask(mask) + mask = self.do_colour_mask(mask, self.opacity) self.buffer_slices[orientation].mask = n_mask final_image = self.do_blend(image, mask) self.buffer_slices[orientation].vtk_mask = mask @@ -496,7 +523,7 @@ class Slice(object): if self.current_mask and self.current_mask.is_shown: n_mask = self.get_mask_slice(orientation, slice_number) mask = converters.to_vtk(n_mask, self.spacing, slice_number, orientation) - mask = self.do_colour_mask(mask) + mask = self.do_colour_mask(mask, self.opacity) final_image = self.do_blend(image, mask) else: n_mask = None @@ -509,6 +536,13 @@ class Slice(object): self.buffer_slices[orientation].vtk_image = image self.buffer_slices[orientation].vtk_mask = mask + if self.to_show_aux == 'watershed' and self.current_mask.is_shown: + m = self.get_aux_slice('watershed', orientation, slice_number) + tmp_vimage = converters.to_vtk(m, self.spacing, slice_number, orientation) + cimage = self.do_custom_colour(tmp_vimage, {0: (0.0, 0.0, 0.0, 0.0), + 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) return final_image def get_image_slice(self, orientation, slice_number, number_slices=1, @@ -701,6 +735,15 @@ class Slice(object): return n_mask + def get_aux_slice(self, name, orientation, n): + m = self.aux_matrices[name] + if orientation == 'AXIAL': + return numpy.array(m[n]) + elif orientation == 'CORONAL': + return numpy.array(m[:, n, :]) + elif orientation == 'SAGITAL': + return numpy.array(m[:, :, n]) + def GetNumberOfSlices(self, orientation): if orientation == 'AXIAL': return self.matrix.shape[0] @@ -1162,6 +1205,19 @@ class Slice(object): m[mask == 254] = 254 return m.astype('uint8') + def do_threshold_to_all_slices(self): + mask = self.current_mask + + # This is very important. Do not use masks' imagedata. It would mess up + # surface quality event when using contour + #self.SetMaskThreshold(mask.index, threshold) + for n in xrange(1, mask.matrix.shape[0]): + if mask.matrix[n, 0, 0] == 0: + m = mask.matrix[n, 1:, 1:] + mask.matrix[n, 1:, 1:] = self.do_threshold_to_a_slice(self.matrix[n-1], m) + + mask.matrix.flush() + def do_colour_image(self, imagedata): if self.from_ in (PLIST, WIDGET): return imagedata @@ -1183,7 +1239,7 @@ class Slice(object): return img_colours_bg.GetOutput() - def do_colour_mask(self, imagedata): + def do_colour_mask(self, imagedata, opacity): scalar_range = int(imagedata.GetScalarRange()[1]) r, g, b = self.current_mask.colour @@ -1197,8 +1253,42 @@ class Slice(object): lut_mask.SetNumberOfTableValues(256) lut_mask.SetTableValue(0, 0, 0, 0, 0.0) lut_mask.SetTableValue(1, 0, 0, 0, 0.0) - lut_mask.SetTableValue(254, r, g, b, 1.0) - lut_mask.SetTableValue(255, r, g, b, 1.0) + lut_mask.SetTableValue(2, 0, 0, 0, 0.0) + lut_mask.SetTableValue(253, r, g, b, opacity) + lut_mask.SetTableValue(254, r, g, b, opacity) + lut_mask.SetTableValue(255, r, g, b, opacity) + lut_mask.SetRampToLinear() + lut_mask.Build() + # self.lut_mask = lut_mask + + # map the input image through a lookup table + img_colours_mask = vtk.vtkImageMapToColors() + img_colours_mask.SetLookupTable(lut_mask) + img_colours_mask.SetOutputFormatToRGBA() + img_colours_mask.SetInput(imagedata) + img_colours_mask.Update() + # self.img_colours_mask = img_colours_mask + + return img_colours_mask.GetOutput() + + def do_custom_colour(self, imagedata, map_colours): + # map scalar values into colors + minv = min(map_colours) + maxv = max(map_colours) + ncolours = maxv - minv + 1 + + lut_mask = vtk.vtkLookupTable() + lut_mask.SetNumberOfColors(ncolours) + lut_mask.SetHueRange(const.THRESHOLD_HUE_RANGE) + lut_mask.SetSaturationRange(1, 1) + lut_mask.SetValueRange(minv, maxv) + lut_mask.SetRange(minv, maxv) + lut_mask.SetNumberOfTableValues(ncolours) + + for v in map_colours: + r,g, b,a = map_colours[v] + lut_mask.SetTableValue(v, r, g, b, a) + lut_mask.SetRampToLinear() lut_mask.Build() # self.lut_mask = lut_mask diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index 33992de..135e69c 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -17,12 +17,22 @@ # detalhes. #-------------------------------------------------------------------------- +import os +import tempfile + import vtk import wx from wx.lib.pubsub import pub as Publisher import constants as const +import converters +import numpy as np + +from scipy import ndimage +from scipy.misc import imsave +from skimage.morphology import watershed +from skimage import filter ORIENTATIONS = { "AXIAL": const.AXIAL, @@ -30,6 +40,20 @@ ORIENTATIONS = { "SAGITAL": const.SAGITAL, } +BRUSH_FOREGROUND=1 +BRUSH_BACKGROUND=2 +BRUSH_ERASE=0 + +WATERSHED_OPERATIONS = {_("Erase"): BRUSH_ERASE, + _("Foreground"): BRUSH_FOREGROUND, + _("Background"): BRUSH_BACKGROUND,} + +def get_LUT_value(data, window, level): + return 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-0)]) + class BaseImageInteractorStyle(vtk.vtkInteractorStyleImage): def __init__(self, viewer): self.right_pressed = False @@ -640,6 +664,394 @@ class EditorInteractorStyle(DefaultInteractorStyle): return x, y, z +class WaterShedInteractorStyle(DefaultInteractorStyle): + matrix = None + def __init__(self, viewer): + DefaultInteractorStyle.__init__(self, viewer) + + self.viewer = viewer + self.orientation = self.viewer.orientation + self.matrix = None + + self.operation = BRUSH_FOREGROUND + + self.mg_size = 3 + + self.picker = vtk.vtkWorldPointPicker() + + self.AddObserver("EnterEvent", self.OnEnterInteractor) + self.AddObserver("LeaveEvent", self.OnLeaveInteractor) + + self.RemoveObservers("MouseWheelForwardEvent") + self.RemoveObservers("MouseWheelBackwardEvent") + self.AddObserver("MouseWheelForwardEvent",self.WOnScrollForward) + self.AddObserver("MouseWheelBackwardEvent", self.WOnScrollBackward) + + self.AddObserver("LeftButtonPressEvent", self.OnBrushClick) + self.AddObserver("LeftButtonReleaseEvent", self.OnBrushRelease) + self.AddObserver("MouseMoveEvent", self.OnBrushMove) + + Publisher.subscribe(self.expand_watershed, 'Expand watershed to 3D ' + self.orientation) + Publisher.subscribe(self.set_operation, 'Set watershed operation') + + def SetUp(self): + self.viewer.slice_.do_threshold_to_all_slices() + mask = self.viewer.slice_.current_mask.matrix + mask[0] = 1 + mask[:, 0, :] = 1 + mask[:, :, 0] = 1 + self._create_mask() + self.viewer.slice_.to_show_aux = 'watershed' + self.viewer.OnScrollBar() + + 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') + self.RemoveAllObservers() + self.viewer.slice_.to_show_aux = '' + self.viewer.OnScrollBar() + + def _create_mask(self): + if self.matrix is None: + try: + self.matrix = self.viewer.slice_.aux_matrices['watershed'] + except KeyError: + self.temp_file, self.matrix = self.viewer.slice_.create_temp_mask() + self.viewer.slice_.aux_matrices['watershed'] = self.matrix + + def _remove_mask(self): + if self.matrix is not None: + self.matrix = None + os.remove(self.temp_file) + print "deleting", self.temp_file + + def set_operation(self, pubsub_evt): + self.operation = WATERSHED_OPERATIONS[pubsub_evt.data] + + def OnEnterInteractor(self, obj, evt): + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + self.viewer.slice_data.cursor.Show() + #self.viewer.interactor.SetCursor(wx.StockCursor(wx.CURSOR_BLANK)) + self.viewer.interactor.Render() + + def OnLeaveInteractor(self, obj, evt): + self.viewer.slice_data.cursor.Show(0) + #self.viewer.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + self.viewer.interactor.Render() + + def WOnScrollBackward(self, obj, evt): + viewer = self.viewer + iren = viewer.interactor + if iren.GetControlKey(): + if viewer.slice_.opacity > 0: + viewer.slice_.opacity -= 0.1 + 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() + viewer.OnScrollBar() + else: + self.OnScrollBackward(obj, evt) + + + def WOnScrollForward(self, obj, evt): + viewer = self.viewer + iren = viewer.interactor + if iren.GetControlKey(): + if viewer.slice_.opacity < 1: + viewer.slice_.opacity += 0.1 + 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() + viewer.OnScrollBar() + else: + self.OnScrollForward(obj, evt) + + + def OnBrushClick(self, obj, evt): + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + viewer = self.viewer + iren = viewer.interactor + + viewer._set_editor_cursor_visibility(1) + + mouse_x, mouse_y = iren.GetEventPosition() + render = iren.FindPokedRenderer(mouse_x, mouse_y) + slice_data = viewer.get_slice_data(render) + + # TODO: Improve! + #for i in self.slice_data_list: + #i.cursor.Show(0) + slice_data.cursor.Show() + + self.picker.Pick(mouse_x, mouse_y, 0, render) + + coord = self.get_coordinate_cursor() + position = slice_data.actor.GetInput().FindPoint(coord) + + if position != -1: + coord = slice_data.actor.GetInput().GetPoint(position) + + slice_data.cursor.SetPosition(coord) + + cursor = slice_data.cursor + position = slice_data.actor.GetInput().FindPoint(coord) + radius = cursor.radius + + if position < 0: + position = viewer.calculate_matrix_position(coord) + + operation = self.operation + + if operation == BRUSH_FOREGROUND: + if iren.GetControlKey(): + operation = BRUSH_BACKGROUND + elif iren.GetShiftKey(): + operation = BRUSH_ERASE + elif operation == BRUSH_BACKGROUND: + if iren.GetControlKey(): + operation = BRUSH_FOREGROUND + elif iren.GetShiftKey(): + operation = BRUSH_ERASE + + n = self.viewer.slice_data.number + self.edit_mask_pixel(operation, n, cursor.GetPixels(), + position, radius, self.orientation) + if self.orientation == 'AXIAL': + mask = self.matrix[n, :, :] + elif self.orientation == 'CORONAL': + mask = self.matrix[:, n, :] + elif self.orientation == 'SAGITAL': + mask = self.matrix[:, :, n] + # TODO: To create a new function to reload images to viewer. + viewer.OnScrollBar() + + def OnBrushMove(self, obj, evt): + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): + return + + viewer = self.viewer + iren = viewer.interactor + + viewer._set_editor_cursor_visibility(1) + + mouse_x, mouse_y = iren.GetEventPosition() + render = iren.FindPokedRenderer(mouse_x, mouse_y) + slice_data = viewer.get_slice_data(render) + + # TODO: Improve! + #for i in self.slice_data_list: + #i.cursor.Show(0) + + self.picker.Pick(mouse_x, mouse_y, 0, render) + + #if (self.pick.GetViewProp()): + #self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_BLANK)) + #else: + #self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + + coord = self.get_coordinate_cursor() + position = viewer.slice_data.actor.GetInput().FindPoint(coord) + + # when position == -1 the cursos is not over the image, so is not + # necessary to set the cursor position to world coordinate center of + # pixel from slice image. + if position != -1: + coord = slice_data.actor.GetInput().GetPoint(position) + slice_data.cursor.SetPosition(coord) + #self.__update_cursor_position(slice_data, coord) + + if (self.left_pressed): + cursor = slice_data.cursor + position = slice_data.actor.GetInput().FindPoint(coord) + radius = cursor.radius + + if position < 0: + position = viewer.calculate_matrix_position(coord) + + operation = self.operation + + if operation == BRUSH_FOREGROUND: + if iren.GetControlKey(): + operation = BRUSH_BACKGROUND + elif iren.GetShiftKey(): + operation = BRUSH_ERASE + elif operation == BRUSH_BACKGROUND: + if iren.GetControlKey(): + operation = BRUSH_FOREGROUND + elif iren.GetShiftKey(): + operation = BRUSH_ERASE + + n = self.viewer.slice_data.number + self.edit_mask_pixel(operation, n, cursor.GetPixels(), + position, radius, self.orientation) + if self.orientation == 'AXIAL': + mask = self.matrix[n, :, :] + elif self.orientation == 'CORONAL': + mask = self.matrix[:, n, :] + elif self.orientation == 'SAGITAL': + mask = self.matrix[:, :, n] + # TODO: To create a new function to reload images to viewer. + viewer.OnScrollBar(update3D=False) + + else: + viewer.interactor.Render() + + def OnBrushRelease(self, evt, obj): + n = self.viewer.slice_data.number + self.viewer.slice_.discard_all_buffers() + if self.orientation == 'AXIAL': + image = self.viewer.slice_.matrix[n] + mask = self.viewer.slice_.current_mask.matrix[n+1, 1:, 1:] + self.viewer.slice_.current_mask.matrix[n+1, 0, 0] = 1 + markers = self.matrix[n] + + elif self.orientation == 'CORONAL': + image = self.viewer.slice_.matrix[:, n, :] + mask = self.viewer.slice_.current_mask.matrix[1:, n+1, 1:] + self.viewer.slice_.current_mask.matrix[0, n+1, 0] + markers = self.matrix[:, n, :] + + elif self.orientation == 'SAGITAL': + image = self.viewer.slice_.matrix[:, :, n] + mask = self.viewer.slice_.current_mask.matrix[1: , 1:, n+1] + 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 + + 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) + + if self.viewer.overwrite_mask: + mask[:] = 0 + mask[tmp_mask == 1] = 253 + else: + mask[(tmp_mask==2) & ((mask == 0) | (mask == 2) | (mask == 253))] = 2 + mask[(tmp_mask==1) & ((mask == 0) | (mask == 2) | (mask == 253))] = 253 + + + self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.clear_history() + Publisher.sendMessage('Reload actual slice') + else: + self.viewer.OnScrollBar(update3D=False) + + def get_coordinate_cursor(self): + # Find position + x, y, z = self.picker.GetPickPosition() + bounds = self.viewer.slice_data.actor.GetBounds() + if bounds[0] == bounds[1]: + x = bounds[0] + elif bounds[2] == bounds[3]: + y = bounds[2] + elif bounds[4] == bounds[5]: + z = bounds[4] + return x, y, z + + def edit_mask_pixel(self, operation, n, index, position, radius, orientation): + if orientation == 'AXIAL': + mask = self.matrix[n, :, :] + elif orientation == 'CORONAL': + mask = self.matrix[:, n, :] + elif orientation == 'SAGITAL': + mask = self.matrix[:, :, n] + + spacing = self.viewer.slice_.spacing + if hasattr(position, '__iter__'): + py, px = position + if orientation == 'AXIAL': + sx = spacing[0] + sy = spacing[1] + elif orientation == 'CORONAL': + sx = spacing[0] + sy = spacing[2] + elif orientation == 'SAGITAL': + sx = spacing[2] + sy = spacing[1] + + else: + if orientation == 'AXIAL': + sx = spacing[0] + sy = spacing[1] + py = position / mask.shape[1] + px = position % mask.shape[1] + elif orientation == 'CORONAL': + sx = spacing[0] + sy = spacing[2] + py = position / mask.shape[1] + px = position % mask.shape[1] + elif orientation == 'SAGITAL': + sx = spacing[2] + sy = spacing[1] + py = position / mask.shape[1] + px = position % mask.shape[1] + + cx = index.shape[1] / 2 + 1 + cy = index.shape[0] / 2 + 1 + xi = px - index.shape[1] + cx + xf = xi + index.shape[1] + yi = py - index.shape[0] + cy + yf = yi + index.shape[0] + + if yi < 0: + index = index[abs(yi):,:] + yi = 0 + if yf > mask.shape[0]: + index = index[:index.shape[0]-(yf-mask.shape[0]), :] + yf = mask.shape[0] + + if xi < 0: + index = index[:,abs(xi):] + xi = 0 + if xf > mask.shape[1]: + index = index[:,:index.shape[1]-(xf-mask.shape[1])] + xf = mask.shape[1] + + # Verifying if the points is over the image array. + if (not 0 <= xi <= mask.shape[1] and not 0 <= xf <= mask.shape[1]) or \ + (not 0 <= yi <= mask.shape[0] and not 0 <= yf <= mask.shape[0]): + return + + roi_m = mask[yi:yf,xi:xf] + + # Checking if roi_i has at least one element. + if roi_m.size: + roi_m[index] = operation + + def expand_watershed(self, pubsub_evt): + markers = self.matrix + image = self.viewer.slice_.matrix + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + 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) + + if self.viewer.overwrite_mask: + mask[:] = 0 + mask[tmp_mask == 1] = 253 + else: + 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_.discard_all_buffers() + self.viewer.slice_.current_mask.clear_history() + Publisher.sendMessage('Reload actual slice') + + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, @@ -653,5 +1065,6 @@ def get_style(style): const.STATE_ZOOM_SL: ZoomSLInteractorStyle, const.SLICE_STATE_SCROLL: ChangeSliceInteractorStyle, const.SLICE_STATE_EDITOR: EditorInteractorStyle, + const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle, } return STYLES[style] diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index dff54a4..3623674 100755 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -166,6 +166,8 @@ class Viewer(wx.Panel): self.last_position_mouse_move = () self.state = const.STATE_DEFAULT + self.overwrite_mask = False + # All renderers and image actors in this viewer self.slice_data_list = [] self.slice_data = None @@ -281,6 +283,8 @@ class Viewer(wx.Panel): if cleanup: self.style.CleanUp() + del self.style + style = styles.get_style(state)(self) setup = getattr(style, 'SetUp', None) @@ -751,6 +755,8 @@ class Viewer(wx.Panel): Publisher.subscribe(self.OnSetMIPInvert, 'Set MIP Invert %s' % self.orientation) Publisher.subscribe(self.OnShowMIPInterface, 'Show MIP interface') + Publisher.subscribe(self.OnSetOverwriteMask, "Set overwrite mask") + def SetDefaultCursor(self, pusub_evt): self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) @@ -1247,7 +1253,10 @@ class Viewer(wx.Panel): self.mip_ctrls.Hide() self.GetSizer().Remove(self.mip_ctrls) self.Layout() - + + def OnSetOverwriteMask(self, pubsub_evt): + value = pubsub_evt.data + self.overwrite_mask = value def set_slice_number(self, index): inverted = self.mip_ctrls.inverted.GetValue() diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 9f5b5c4..ac0022e 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -26,6 +26,8 @@ import webbrowser import wx import wx.aui from wx.lib.pubsub import pub as Publisher +import wx.lib.agw.toasterbox as TB +import wx.lib.popupctl as pc import constants as const import default_tasks as tasks @@ -45,6 +47,19 @@ VIEW_TOOLS = [ID_LAYOUT, ID_TEXT] =\ +class MessageWatershed(wx.PopupWindow): + def __init__(self, prnt, msg): + wx.PopupWindow.__init__(self, prnt, -1) + self.txt = wx.StaticText(self, -1, msg) + + self.sizer = wx.BoxSizer(wx.HORIZONTAL) + self.sizer.Add(self.txt, 1, wx.EXPAND) + self.SetSizer(self.sizer) + + self.sizer.Fit(self) + self.Layout() + self.Update() + self.SetAutoLayout(1) @@ -64,6 +79,8 @@ class Frame(wx.Frame): self.Center(wx.BOTH) icon_path = os.path.join(const.ICON_DIR, "invesalius.ico") self.SetIcon(wx.Icon(icon_path, wx.BITMAP_TYPE_ICO)) + + self.mw = None if sys.platform != 'darwin': self.Maximize() @@ -104,6 +121,7 @@ class Frame(wx.Frame): sub(self._SetProjectName, 'Set project name') sub(self._ShowContentPanel, 'Show content panel') sub(self._ShowImportPanel, 'Show import panel in frame') + #sub(self._ShowHelpMessage, 'Show help message') sub(self._ShowImportNetwork, 'Show retrieve dicom panel') sub(self._ShowTask, 'Show task panel') sub(self._UpdateAUI, 'Update AUI') @@ -116,6 +134,7 @@ class Frame(wx.Frame): self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_MENU, self.OnMenuClick) self.Bind(wx.EVT_CLOSE, self.OnClose) + #self.Bind(wx.EVT_MOVE, self.OnMove) def __init_aui(self): """ @@ -289,6 +308,14 @@ class Frame(wx.Frame): aui_manager.GetPane("Import").Show(0) aui_manager.Update() + def _ShowHelpMessage(self, evt_pubsub): + aui_manager = self.aui_manager + pos = aui_manager.GetPane("Data").window.GetScreenPosition() + msg = evt_pubsub.data + self.mw = MessageWatershed(self, msg) + self.mw.SetPosition(pos) + self.mw.Show() + def _ShowImportPanel(self, evt_pubsub): """ Show only DICOM import panel. @@ -378,6 +405,12 @@ class Frame(wx.Frame): Publisher.sendMessage(('ProgressBar Reposition')) evt.Skip() + + def OnMove(self, evt): + aui_manager = self.aui_manager + pos = aui_manager.GetPane("Data").window.GetScreenPosition() + self.mw.SetPosition(pos) + def ShowPreferences(self): if self.preferences.ShowModal() == wx.ID_OK: diff --git a/invesalius/gui/task_slice.py b/invesalius/gui/task_slice.py index ea81462..8519536 100644 --- a/invesalius/gui/task_slice.py +++ b/invesalius/gui/task_slice.py @@ -228,7 +228,7 @@ class InnerFoldPanel(wx.Panel): # parent panel. Perhaps we need to insert the item into the sizer also... # Study this. fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, - (10, 190), 0,fpb.FPB_SINGLE_FOLD) + (10, 220), 0,fpb.FPB_SINGLE_FOLD) # Fold panel style style = fpb.CaptionBarStyle() @@ -252,6 +252,13 @@ class InnerFoldPanel(wx.Panel): self.__id_editor = item.GetId() self.last_panel_opened = None + # Fold 3 - Watershed + item = fold_panel.AddFoldPanel(_("Watershed"), collapsed=True) + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, WatershedTool(item), Spacing= 0, + leftSpacing=0, rightSpacing=0) + self.__id_watershed = item.GetId() + #fold_panel.Expand(fold_panel.GetFoldPanel(1)) # Panel sizer to expand fold panel @@ -274,6 +281,7 @@ class InnerFoldPanel(wx.Panel): def __bind_pubsub_evt(self): Publisher.subscribe(self.OnRetrieveStyle, 'Retrieve task slice style') Publisher.subscribe(self.OnDisableStyle, 'Disable task slice style') + Publisher.subscribe(self.OnCloseProject, 'Close project data') def OnFoldPressCaption(self, evt): id = evt.GetTag().GetId() @@ -284,8 +292,18 @@ class InnerFoldPanel(wx.Panel): Publisher.sendMessage('Disable style', const.SLICE_STATE_EDITOR) self.last_style = None else: - Publisher.sendMessage('Enable style', const.SLICE_STATE_EDITOR) + Publisher.sendMessage('Enable style', + const.SLICE_STATE_EDITOR) self.last_style = const.SLICE_STATE_EDITOR + elif self.__id_watershed == id: + if closed: + Publisher.sendMessage('Disable style', + const.SLICE_STATE_WATERSHED) + self.last_style = None + else: + Publisher.sendMessage('Enable style', const.SLICE_STATE_WATERSHED) + Publisher.sendMessage('Show help message', 'Mark the object and the background') + self.last_style = const.SLICE_STATE_WATERSHED else: Publisher.sendMessage('Disable style', const.SLICE_STATE_EDITOR) self.last_style = None @@ -300,6 +318,9 @@ class InnerFoldPanel(wx.Panel): if (self.last_style == const.SLICE_STATE_EDITOR): Publisher.sendMessage('Disable style', const.SLICE_STATE_EDITOR) + def OnCloseProject(self, pubsub_evt): + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) + def GetMaskSelected(self): x= self.mask_prop_panel.GetMaskSelected() return self.mask_prop_panel.GetMaskSelected() @@ -688,3 +709,143 @@ class EditionTools(wx.Panel): Publisher.sendMessage('Set edition operation', brush_op_id) +class WatershedTool(EditionTools): + def __init__(self, parent): + wx.Panel.__init__(self, parent, size=(50,150)) + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + self.SetBackgroundColour(default_colour) + + ## LINE 1 + text1 = wx.StaticText(self, -1, _("Choose brush type and size:")) + + ## LINE 2 + menu = wx.Menu() + + CIRCLE_BMP = wx.Bitmap("../icons/brush_circle.jpg", wx.BITMAP_TYPE_JPEG) + item = wx.MenuItem(menu, MENU_BRUSH_CIRCLE, _("Circle")) + item.SetBitmap(CIRCLE_BMP) + + SQUARE_BMP = wx.Bitmap("../icons/brush_square.jpg", wx.BITMAP_TYPE_JPEG) + item2 = wx.MenuItem(menu, MENU_BRUSH_SQUARE, _("Square")) + item2.SetBitmap(SQUARE_BMP) + + menu.AppendItem(item) + menu.AppendItem(item2) + + bmp_brush_format = {const.BRUSH_CIRCLE: CIRCLE_BMP, + const.BRUSH_SQUARE: SQUARE_BMP} + selected_bmp = bmp_brush_format[const.DEFAULT_BRUSH_FORMAT] + + btn_brush_format = pbtn.PlateButton(self, wx.ID_ANY,"", selected_bmp, + style=pbtn.PB_STYLE_SQUARE) + btn_brush_format.SetMenu(menu) + self.btn_brush_format = btn_brush_format + + spin_brush_size = wx.SpinCtrl(self, -1, "", (20, 50)) + spin_brush_size.SetRange(1,100) + spin_brush_size.SetValue(const.BRUSH_SIZE) + spin_brush_size.Bind(wx.EVT_TEXT, self.OnBrushSize) + self.spin = spin_brush_size + + combo_brush_op = wx.ComboBox(self, -1, "", size=(15,-1), + choices = (_("Foreground"), + _("Background"), + _("Erase")), + style = wx.CB_DROPDOWN|wx.CB_READONLY) + combo_brush_op.SetSelection(0) + if sys.platform != 'win32': + combo_brush_op.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + self.combo_brush_op = combo_brush_op + + # Sizer which represents the second line + line2 = wx.BoxSizer(wx.HORIZONTAL) + line2.Add(btn_brush_format, 0, wx.EXPAND|wx.GROW|wx.TOP|wx.RIGHT, 0) + line2.Add(spin_brush_size, 0, wx.RIGHT, 5) + line2.Add(combo_brush_op, 1, wx.EXPAND|wx.TOP|wx.RIGHT|wx.LEFT, 5) + + ## LINE 3 + + ## LINE 4 + + # LINE 5 + check_box = wx.CheckBox(self, -1, _("Overwrite mask")) + self.check_box = check_box + + # Line 6 + self.btn_exp_watershed = wx.Button(self, -1, _('Expand watershed to 3D')) + + # 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.Fit(self) + + self.SetSizer(sizer) + self.Update() + self.SetAutoLayout(1) + + self.__bind_events_wx() + + + def __bind_events_wx(self): + 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.btn_exp_watershed.Bind(wx.EVT_BUTTON, self.OnExpandWatershed) + + def ChangeMaskColour(self, pubsub_evt): + colour = pubsub_evt.data + self.gradient_thresh.SetColour(colour) + + def SetGradientColour(self, pubsub_evt): + vtk_colour = pubsub_evt.data[3] + wx_colour = [c*255 for c in vtk_colour] + self.gradient_thresh.SetColour(wx_colour) + + def SetThresholdValues(self, pubsub_evt): + thresh_min, thresh_max = pubsub_evt.data + self.bind_evt_gradient = False + self.gradient_thresh.SetMinValue(thresh_min) + self.gradient_thresh.SetMaxValue(thresh_max) + self.bind_evt_gradient = True + + def SetThresholdBounds(self, pubsub_evt): + thresh_min = pubsub_evt.data[0] + thresh_max = pubsub_evt.data[1] + self.gradient_thresh.SetMinRange(thresh_min) + self.gradient_thresh.SetMaxRange(thresh_max) + self.gradient_thresh.SetMinValue(thresh_min) + self.gradient_thresh.SetMaxValue(thresh_max) + + def OnMenu(self, evt): + SQUARE_BMP = wx.Bitmap("../icons/brush_square.jpg", wx.BITMAP_TYPE_JPEG) + CIRCLE_BMP = wx.Bitmap("../icons/brush_circle.jpg", wx.BITMAP_TYPE_JPEG) + + brush = {MENU_BRUSH_CIRCLE: const.BRUSH_CIRCLE, + MENU_BRUSH_SQUARE: const.BRUSH_SQUARE} + bitmap = {MENU_BRUSH_CIRCLE: CIRCLE_BMP, + MENU_BRUSH_SQUARE: SQUARE_BMP} + + self.btn_brush_format.SetBitmap(bitmap[evt.GetId()]) + + Publisher.sendMessage('Set 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()) + + def OnComboBrushOp(self, evt): + brush_op = self.combo_brush_op.GetValue() + Publisher.sendMessage('Set watershed operation', brush_op) + + def OnCheckOverwriteMask(self, evt): + value = self.check_box.GetValue() + Publisher.sendMessage('Set overwrite mask', value) + + def OnExpandWatershed(self, evt): + Publisher.sendMessage('Expand watershed to 3D AXIAL') -- libgit2 0.21.2