Commit c180da8d50612b70e749b30d35a8f881d0453397

Authored by Thiago Franco de Moraes
1 parent 9c8083ac

This implements the segmentation watershed algorithm to InVesalius.

Watershed is a very useful algorithm to segment brain, for example. The
implementation used is from scikit image. Before the segmentation, a
ww&wl is applied and a morphological gradient is applied, to extract the
borders of the image. It can be used to 2D slices or to all slices (via
expand button). It was created a new slice interaction style: WaterShedInteractorStyle.

Starting to using watershed

Added suport to erase a mark

Applying threshold before starting watershed mode

Watershed 3D (only markers from axial)

Watershed 3D

Marking the mask as edited when using watershed tool

Fixed the problem of not deleting watershed interactor style

Not flushing buffer (it's not necessary)

Not hidding wx's cursor when marking to watershed

Removed the vtk colour part from watershed style

Removed the vtk colour part from watershed style

micro-optimization

Removing aux matrix when opening a new dicom dir

not showing watershed markers when mask is hidden

Only doing watershed when user marks back and foreground.

Reloading all mask slice when watershed is expanded

showing (hidding) watershed marks when enabled (disabled)

cleaning undo redo when watershed is applied (TODO create undo redo to watershed)

Interface to select watershed operation
invesalius/constants.py
... ... @@ -485,6 +485,7 @@ STATE_MEASURE_ANGLE = 1008
485 485 SLICE_STATE_CROSS = 3006
486 486 SLICE_STATE_SCROLL = 3007
487 487 SLICE_STATE_EDITOR = 3008
  488 +SLICE_STATE_WATERSHED = 3009
488 489  
489 490 VOLUME_STATE_SEED = 2001
490 491 #STATE_LINEAR_MEASURE = 3001
... ... @@ -500,6 +501,7 @@ TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL]
500 501 SLICE_STYLES = TOOL_STATES + TOOL_SLICE_STATES
501 502 SLICE_STYLES.append(STATE_DEFAULT)
502 503 SLICE_STYLES.append(SLICE_STATE_EDITOR)
  504 +SLICE_STYLES.append(SLICE_STATE_WATERSHED)
503 505  
504 506 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE,
505 507 STATE_MEASURE_ANGLE]
... ... @@ -507,6 +509,7 @@ VOLUME_STYLES.append(STATE_DEFAULT)
507 509  
508 510  
509 511 STYLE_LEVEL = {SLICE_STATE_EDITOR: 1,
  512 + SLICE_STATE_WATERSHED: 1,
510 513 SLICE_STATE_CROSS: 2,
511 514 SLICE_STATE_SCROLL: 2,
512 515 STATE_ANNOTATE: 2,
... ...
invesalius/control.py
... ... @@ -127,6 +127,11 @@ class Controller():
127 127 answer = dialog.SaveChangesDialog2(filename)
128 128 if answer:
129 129 self.ShowDialogSaveProject()
  130 + self.CloseProject()
  131 + #Publisher.sendMessage("Enable state project", False)
  132 + Publisher.sendMessage('Set project name')
  133 + Publisher.sendMessage("Stop Config Recording")
  134 + Publisher.sendMessage("Set slice interaction style", const.STATE_DEFAULT)
130 135 # Import project
131 136 dirpath = dialog.ShowImportDirDialog()
132 137 if dirpath and not os.listdir(dirpath):
... ...
invesalius/data/slice_.py
... ... @@ -84,6 +84,10 @@ class Slice(object):
84 84 self.blend_filter = None
85 85 self.histogram = None
86 86 self._matrix = None
  87 + self.aux_matrices = {}
  88 + self.state = const.STATE_DEFAULT
  89 +
  90 + self.to_show_aux = ''
87 91  
88 92 self._type_projection = const.PROJECTION_NORMAL
89 93 self.n_border = const.PROJECTION_BORDER_SIZE
... ... @@ -107,6 +111,7 @@ class Slice(object):
107 111  
108 112 self.from_ = OTHER
109 113 self.__bind_events()
  114 + self.opacity = 0.8
110 115  
111 116 @property
112 117 def matrix(self):
... ... @@ -187,6 +192,11 @@ class Slice(object):
187 192 elif orientation == 'SAGITAL':
188 193 return shape[2] - 1
189 194  
  195 + def discard_all_buffers(self):
  196 + for buffer_ in self.buffer_slices.values():
  197 + buffer_.discard_vtk_mask()
  198 + buffer_.discard_mask()
  199 +
190 200 def OnRemoveMasks(self, pubsub_evt):
191 201 selected_items = pubsub_evt.data
192 202 proj = Project()
... ... @@ -228,6 +238,7 @@ class Slice(object):
228 238 if (state in const.SLICE_STYLES):
229 239 new_state = self.interaction_style.AddState(state)
230 240 Publisher.sendMessage('Set slice interaction style', new_state)
  241 + self.state = state
231 242  
232 243 def OnDisableStyle(self, pubsub_evt):
233 244 state = pubsub_evt.data
... ... @@ -237,6 +248,7 @@ class Slice(object):
237 248  
238 249 if (state == const.SLICE_STATE_EDITOR):
239 250 Publisher.sendMessage('Set interactor default cursor')
  251 + self.state = new_state
240 252  
241 253 def OnCloseProject(self, pubsub_evt):
242 254 self.CloseProject()
... ... @@ -249,9 +261,18 @@ class Slice(object):
249 261 os.remove(f)
250 262 self.current_mask = None
251 263  
  264 + for name in self.aux_matrices:
  265 + m = self.aux_matrices[name]
  266 + f = m.filename
  267 + m._mmap.close()
  268 + m = None
  269 + os.remove(f)
  270 + self.aux_matrices = {}
  271 +
252 272 self.values = None
253 273 self.nodes = None
254 274 self.from_= OTHER
  275 + self.state = const.STATE_DEFAULT
255 276  
256 277 self.number_of_colours = 256
257 278 self.saturation_range = (0, 0)
... ... @@ -380,6 +401,12 @@ class Slice(object):
380 401 value = False
381 402 Publisher.sendMessage('Show mask', (index, value))
382 403  
  404 + def create_temp_mask(self):
  405 + temp_file = tempfile.mktemp()
  406 + shape = self.matrix.shape
  407 + matrix = numpy.memmap(temp_file, mode='w+', dtype='uint8', shape=shape)
  408 + return temp_file, matrix
  409 +
383 410 def edit_mask_pixel(self, operation, index, position, radius, orientation):
384 411 mask = self.buffer_slices[orientation].mask
385 412 image = self.buffer_slices[orientation].image
... ... @@ -479,7 +506,7 @@ class Slice(object):
479 506 print "Do not getting from buffer"
480 507 n_mask = self.get_mask_slice(orientation, slice_number)
481 508 mask = converters.to_vtk(n_mask, self.spacing, slice_number, orientation)
482   - mask = self.do_colour_mask(mask)
  509 + mask = self.do_colour_mask(mask, self.opacity)
483 510 self.buffer_slices[orientation].mask = n_mask
484 511 final_image = self.do_blend(image, mask)
485 512 self.buffer_slices[orientation].vtk_mask = mask
... ... @@ -496,7 +523,7 @@ class Slice(object):
496 523 if self.current_mask and self.current_mask.is_shown:
497 524 n_mask = self.get_mask_slice(orientation, slice_number)
498 525 mask = converters.to_vtk(n_mask, self.spacing, slice_number, orientation)
499   - mask = self.do_colour_mask(mask)
  526 + mask = self.do_colour_mask(mask, self.opacity)
500 527 final_image = self.do_blend(image, mask)
501 528 else:
502 529 n_mask = None
... ... @@ -509,6 +536,13 @@ class Slice(object):
509 536 self.buffer_slices[orientation].vtk_image = image
510 537 self.buffer_slices[orientation].vtk_mask = mask
511 538  
  539 + if self.to_show_aux == 'watershed' and self.current_mask.is_shown:
  540 + m = self.get_aux_slice('watershed', orientation, slice_number)
  541 + tmp_vimage = converters.to_vtk(m, self.spacing, slice_number, orientation)
  542 + cimage = self.do_custom_colour(tmp_vimage, {0: (0.0, 0.0, 0.0, 0.0),
  543 + 1: (0.0, 1.0, 0.0, 1.0),
  544 + 2: (1.0, 0.0, 0.0, 1.0)})
  545 + final_image = self.do_blend(final_image, cimage)
512 546 return final_image
513 547  
514 548 def get_image_slice(self, orientation, slice_number, number_slices=1,
... ... @@ -701,6 +735,15 @@ class Slice(object):
701 735  
702 736 return n_mask
703 737  
  738 + def get_aux_slice(self, name, orientation, n):
  739 + m = self.aux_matrices[name]
  740 + if orientation == 'AXIAL':
  741 + return numpy.array(m[n])
  742 + elif orientation == 'CORONAL':
  743 + return numpy.array(m[:, n, :])
  744 + elif orientation == 'SAGITAL':
  745 + return numpy.array(m[:, :, n])
  746 +
704 747 def GetNumberOfSlices(self, orientation):
705 748 if orientation == 'AXIAL':
706 749 return self.matrix.shape[0]
... ... @@ -1162,6 +1205,19 @@ class Slice(object):
1162 1205 m[mask == 254] = 254
1163 1206 return m.astype('uint8')
1164 1207  
  1208 + def do_threshold_to_all_slices(self):
  1209 + mask = self.current_mask
  1210 +
  1211 + # This is very important. Do not use masks' imagedata. It would mess up
  1212 + # surface quality event when using contour
  1213 + #self.SetMaskThreshold(mask.index, threshold)
  1214 + for n in xrange(1, mask.matrix.shape[0]):
  1215 + if mask.matrix[n, 0, 0] == 0:
  1216 + m = mask.matrix[n, 1:, 1:]
  1217 + mask.matrix[n, 1:, 1:] = self.do_threshold_to_a_slice(self.matrix[n-1], m)
  1218 +
  1219 + mask.matrix.flush()
  1220 +
1165 1221 def do_colour_image(self, imagedata):
1166 1222 if self.from_ in (PLIST, WIDGET):
1167 1223 return imagedata
... ... @@ -1183,7 +1239,7 @@ class Slice(object):
1183 1239  
1184 1240 return img_colours_bg.GetOutput()
1185 1241  
1186   - def do_colour_mask(self, imagedata):
  1242 + def do_colour_mask(self, imagedata, opacity):
1187 1243 scalar_range = int(imagedata.GetScalarRange()[1])
1188 1244 r, g, b = self.current_mask.colour
1189 1245  
... ... @@ -1197,8 +1253,42 @@ class Slice(object):
1197 1253 lut_mask.SetNumberOfTableValues(256)
1198 1254 lut_mask.SetTableValue(0, 0, 0, 0, 0.0)
1199 1255 lut_mask.SetTableValue(1, 0, 0, 0, 0.0)
1200   - lut_mask.SetTableValue(254, r, g, b, 1.0)
1201   - lut_mask.SetTableValue(255, r, g, b, 1.0)
  1256 + lut_mask.SetTableValue(2, 0, 0, 0, 0.0)
  1257 + lut_mask.SetTableValue(253, r, g, b, opacity)
  1258 + lut_mask.SetTableValue(254, r, g, b, opacity)
  1259 + lut_mask.SetTableValue(255, r, g, b, opacity)
  1260 + lut_mask.SetRampToLinear()
  1261 + lut_mask.Build()
  1262 + # self.lut_mask = lut_mask
  1263 +
  1264 + # map the input image through a lookup table
  1265 + img_colours_mask = vtk.vtkImageMapToColors()
  1266 + img_colours_mask.SetLookupTable(lut_mask)
  1267 + img_colours_mask.SetOutputFormatToRGBA()
  1268 + img_colours_mask.SetInput(imagedata)
  1269 + img_colours_mask.Update()
  1270 + # self.img_colours_mask = img_colours_mask
  1271 +
  1272 + return img_colours_mask.GetOutput()
  1273 +
  1274 + def do_custom_colour(self, imagedata, map_colours):
  1275 + # map scalar values into colors
  1276 + minv = min(map_colours)
  1277 + maxv = max(map_colours)
  1278 + ncolours = maxv - minv + 1
  1279 +
  1280 + lut_mask = vtk.vtkLookupTable()
  1281 + lut_mask.SetNumberOfColors(ncolours)
  1282 + lut_mask.SetHueRange(const.THRESHOLD_HUE_RANGE)
  1283 + lut_mask.SetSaturationRange(1, 1)
  1284 + lut_mask.SetValueRange(minv, maxv)
  1285 + lut_mask.SetRange(minv, maxv)
  1286 + lut_mask.SetNumberOfTableValues(ncolours)
  1287 +
  1288 + for v in map_colours:
  1289 + r,g, b,a = map_colours[v]
  1290 + lut_mask.SetTableValue(v, r, g, b, a)
  1291 +
1202 1292 lut_mask.SetRampToLinear()
1203 1293 lut_mask.Build()
1204 1294 # self.lut_mask = lut_mask
... ...
invesalius/data/styles.py
... ... @@ -17,12 +17,22 @@
17 17 # detalhes.
18 18 #--------------------------------------------------------------------------
19 19  
  20 +import os
  21 +import tempfile
  22 +
20 23 import vtk
21 24 import wx
22 25  
23 26 from wx.lib.pubsub import pub as Publisher
24 27  
25 28 import constants as const
  29 +import converters
  30 +import numpy as np
  31 +
  32 +from scipy import ndimage
  33 +from scipy.misc import imsave
  34 +from skimage.morphology import watershed
  35 +from skimage import filter
26 36  
27 37 ORIENTATIONS = {
28 38 "AXIAL": const.AXIAL,
... ... @@ -30,6 +40,20 @@ ORIENTATIONS = {
30 40 "SAGITAL": const.SAGITAL,
31 41 }
32 42  
  43 +BRUSH_FOREGROUND=1
  44 +BRUSH_BACKGROUND=2
  45 +BRUSH_ERASE=0
  46 +
  47 +WATERSHED_OPERATIONS = {_("Erase"): BRUSH_ERASE,
  48 + _("Foreground"): BRUSH_FOREGROUND,
  49 + _("Background"): BRUSH_BACKGROUND,}
  50 +
  51 +def get_LUT_value(data, window, level):
  52 + return np.piecewise(data,
  53 + [data <= (level - 0.5 - (window-1)/2),
  54 + data > (level - 0.5 + (window-1)/2)],
  55 + [0, 255, lambda data: ((data - (level - 0.5))/(window-1) + 0.5)*(255-0)])
  56 +
33 57 class BaseImageInteractorStyle(vtk.vtkInteractorStyleImage):
34 58 def __init__(self, viewer):
35 59 self.right_pressed = False
... ... @@ -640,6 +664,394 @@ class EditorInteractorStyle(DefaultInteractorStyle):
640 664 return x, y, z
641 665  
642 666  
  667 +class WaterShedInteractorStyle(DefaultInteractorStyle):
  668 + matrix = None
  669 + def __init__(self, viewer):
  670 + DefaultInteractorStyle.__init__(self, viewer)
  671 +
  672 + self.viewer = viewer
  673 + self.orientation = self.viewer.orientation
  674 + self.matrix = None
  675 +
  676 + self.operation = BRUSH_FOREGROUND
  677 +
  678 + self.mg_size = 3
  679 +
  680 + self.picker = vtk.vtkWorldPointPicker()
  681 +
  682 + self.AddObserver("EnterEvent", self.OnEnterInteractor)
  683 + self.AddObserver("LeaveEvent", self.OnLeaveInteractor)
  684 +
  685 + self.RemoveObservers("MouseWheelForwardEvent")
  686 + self.RemoveObservers("MouseWheelBackwardEvent")
  687 + self.AddObserver("MouseWheelForwardEvent",self.WOnScrollForward)
  688 + self.AddObserver("MouseWheelBackwardEvent", self.WOnScrollBackward)
  689 +
  690 + self.AddObserver("LeftButtonPressEvent", self.OnBrushClick)
  691 + self.AddObserver("LeftButtonReleaseEvent", self.OnBrushRelease)
  692 + self.AddObserver("MouseMoveEvent", self.OnBrushMove)
  693 +
  694 + Publisher.subscribe(self.expand_watershed, 'Expand watershed to 3D ' + self.orientation)
  695 + Publisher.subscribe(self.set_operation, 'Set watershed operation')
  696 +
  697 + def SetUp(self):
  698 + self.viewer.slice_.do_threshold_to_all_slices()
  699 + mask = self.viewer.slice_.current_mask.matrix
  700 + mask[0] = 1
  701 + mask[:, 0, :] = 1
  702 + mask[:, :, 0] = 1
  703 + self._create_mask()
  704 + self.viewer.slice_.to_show_aux = 'watershed'
  705 + self.viewer.OnScrollBar()
  706 +
  707 + def CleanUp(self):
  708 + #self._remove_mask()
  709 + Publisher.unsubscribe(self.expand_watershed, 'Expand watershed to 3D ' + self.orientation)
  710 + Publisher.unsubscribe(self.set_operation, 'Set watershed operation')
  711 + self.RemoveAllObservers()
  712 + self.viewer.slice_.to_show_aux = ''
  713 + self.viewer.OnScrollBar()
  714 +
  715 + def _create_mask(self):
  716 + if self.matrix is None:
  717 + try:
  718 + self.matrix = self.viewer.slice_.aux_matrices['watershed']
  719 + except KeyError:
  720 + self.temp_file, self.matrix = self.viewer.slice_.create_temp_mask()
  721 + self.viewer.slice_.aux_matrices['watershed'] = self.matrix
  722 +
  723 + def _remove_mask(self):
  724 + if self.matrix is not None:
  725 + self.matrix = None
  726 + os.remove(self.temp_file)
  727 + print "deleting", self.temp_file
  728 +
  729 + def set_operation(self, pubsub_evt):
  730 + self.operation = WATERSHED_OPERATIONS[pubsub_evt.data]
  731 +
  732 + def OnEnterInteractor(self, obj, evt):
  733 + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None):
  734 + return
  735 + self.viewer.slice_data.cursor.Show()
  736 + #self.viewer.interactor.SetCursor(wx.StockCursor(wx.CURSOR_BLANK))
  737 + self.viewer.interactor.Render()
  738 +
  739 + def OnLeaveInteractor(self, obj, evt):
  740 + self.viewer.slice_data.cursor.Show(0)
  741 + #self.viewer.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
  742 + self.viewer.interactor.Render()
  743 +
  744 + def WOnScrollBackward(self, obj, evt):
  745 + viewer = self.viewer
  746 + iren = viewer.interactor
  747 + if iren.GetControlKey():
  748 + if viewer.slice_.opacity > 0:
  749 + viewer.slice_.opacity -= 0.1
  750 + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask()
  751 + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask()
  752 + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
  753 + viewer.OnScrollBar()
  754 + else:
  755 + self.OnScrollBackward(obj, evt)
  756 +
  757 +
  758 + def WOnScrollForward(self, obj, evt):
  759 + viewer = self.viewer
  760 + iren = viewer.interactor
  761 + if iren.GetControlKey():
  762 + if viewer.slice_.opacity < 1:
  763 + viewer.slice_.opacity += 0.1
  764 + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask()
  765 + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask()
  766 + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
  767 + viewer.OnScrollBar()
  768 + else:
  769 + self.OnScrollForward(obj, evt)
  770 +
  771 +
  772 + def OnBrushClick(self, obj, evt):
  773 + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None):
  774 + return
  775 +
  776 + viewer = self.viewer
  777 + iren = viewer.interactor
  778 +
  779 + viewer._set_editor_cursor_visibility(1)
  780 +
  781 + mouse_x, mouse_y = iren.GetEventPosition()
  782 + render = iren.FindPokedRenderer(mouse_x, mouse_y)
  783 + slice_data = viewer.get_slice_data(render)
  784 +
  785 + # TODO: Improve!
  786 + #for i in self.slice_data_list:
  787 + #i.cursor.Show(0)
  788 + slice_data.cursor.Show()
  789 +
  790 + self.picker.Pick(mouse_x, mouse_y, 0, render)
  791 +
  792 + coord = self.get_coordinate_cursor()
  793 + position = slice_data.actor.GetInput().FindPoint(coord)
  794 +
  795 + if position != -1:
  796 + coord = slice_data.actor.GetInput().GetPoint(position)
  797 +
  798 + slice_data.cursor.SetPosition(coord)
  799 +
  800 + cursor = slice_data.cursor
  801 + position = slice_data.actor.GetInput().FindPoint(coord)
  802 + radius = cursor.radius
  803 +
  804 + if position < 0:
  805 + position = viewer.calculate_matrix_position(coord)
  806 +
  807 + operation = self.operation
  808 +
  809 + if operation == BRUSH_FOREGROUND:
  810 + if iren.GetControlKey():
  811 + operation = BRUSH_BACKGROUND
  812 + elif iren.GetShiftKey():
  813 + operation = BRUSH_ERASE
  814 + elif operation == BRUSH_BACKGROUND:
  815 + if iren.GetControlKey():
  816 + operation = BRUSH_FOREGROUND
  817 + elif iren.GetShiftKey():
  818 + operation = BRUSH_ERASE
  819 +
  820 + n = self.viewer.slice_data.number
  821 + self.edit_mask_pixel(operation, n, cursor.GetPixels(),
  822 + position, radius, self.orientation)
  823 + if self.orientation == 'AXIAL':
  824 + mask = self.matrix[n, :, :]
  825 + elif self.orientation == 'CORONAL':
  826 + mask = self.matrix[:, n, :]
  827 + elif self.orientation == 'SAGITAL':
  828 + mask = self.matrix[:, :, n]
  829 + # TODO: To create a new function to reload images to viewer.
  830 + viewer.OnScrollBar()
  831 +
  832 + def OnBrushMove(self, obj, evt):
  833 + if (self.viewer.slice_.buffer_slices[self.orientation].mask is None):
  834 + return
  835 +
  836 + viewer = self.viewer
  837 + iren = viewer.interactor
  838 +
  839 + viewer._set_editor_cursor_visibility(1)
  840 +
  841 + mouse_x, mouse_y = iren.GetEventPosition()
  842 + render = iren.FindPokedRenderer(mouse_x, mouse_y)
  843 + slice_data = viewer.get_slice_data(render)
  844 +
  845 + # TODO: Improve!
  846 + #for i in self.slice_data_list:
  847 + #i.cursor.Show(0)
  848 +
  849 + self.picker.Pick(mouse_x, mouse_y, 0, render)
  850 +
  851 + #if (self.pick.GetViewProp()):
  852 + #self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_BLANK))
  853 + #else:
  854 + #self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
  855 +
  856 + coord = self.get_coordinate_cursor()
  857 + position = viewer.slice_data.actor.GetInput().FindPoint(coord)
  858 +
  859 + # when position == -1 the cursos is not over the image, so is not
  860 + # necessary to set the cursor position to world coordinate center of
  861 + # pixel from slice image.
  862 + if position != -1:
  863 + coord = slice_data.actor.GetInput().GetPoint(position)
  864 + slice_data.cursor.SetPosition(coord)
  865 + #self.__update_cursor_position(slice_data, coord)
  866 +
  867 + if (self.left_pressed):
  868 + cursor = slice_data.cursor
  869 + position = slice_data.actor.GetInput().FindPoint(coord)
  870 + radius = cursor.radius
  871 +
  872 + if position < 0:
  873 + position = viewer.calculate_matrix_position(coord)
  874 +
  875 + operation = self.operation
  876 +
  877 + if operation == BRUSH_FOREGROUND:
  878 + if iren.GetControlKey():
  879 + operation = BRUSH_BACKGROUND
  880 + elif iren.GetShiftKey():
  881 + operation = BRUSH_ERASE
  882 + elif operation == BRUSH_BACKGROUND:
  883 + if iren.GetControlKey():
  884 + operation = BRUSH_FOREGROUND
  885 + elif iren.GetShiftKey():
  886 + operation = BRUSH_ERASE
  887 +
  888 + n = self.viewer.slice_data.number
  889 + self.edit_mask_pixel(operation, n, cursor.GetPixels(),
  890 + position, radius, self.orientation)
  891 + if self.orientation == 'AXIAL':
  892 + mask = self.matrix[n, :, :]
  893 + elif self.orientation == 'CORONAL':
  894 + mask = self.matrix[:, n, :]
  895 + elif self.orientation == 'SAGITAL':
  896 + mask = self.matrix[:, :, n]
  897 + # TODO: To create a new function to reload images to viewer.
  898 + viewer.OnScrollBar(update3D=False)
  899 +
  900 + else:
  901 + viewer.interactor.Render()
  902 +
  903 + def OnBrushRelease(self, evt, obj):
  904 + n = self.viewer.slice_data.number
  905 + self.viewer.slice_.discard_all_buffers()
  906 + if self.orientation == 'AXIAL':
  907 + image = self.viewer.slice_.matrix[n]
  908 + mask = self.viewer.slice_.current_mask.matrix[n+1, 1:, 1:]
  909 + self.viewer.slice_.current_mask.matrix[n+1, 0, 0] = 1
  910 + markers = self.matrix[n]
  911 +
  912 + elif self.orientation == 'CORONAL':
  913 + image = self.viewer.slice_.matrix[:, n, :]
  914 + mask = self.viewer.slice_.current_mask.matrix[1:, n+1, 1:]
  915 + self.viewer.slice_.current_mask.matrix[0, n+1, 0]
  916 + markers = self.matrix[:, n, :]
  917 +
  918 + elif self.orientation == 'SAGITAL':
  919 + image = self.viewer.slice_.matrix[:, :, n]
  920 + mask = self.viewer.slice_.current_mask.matrix[1: , 1:, n+1]
  921 + self.viewer.slice_.current_mask.matrix[0 , 0, n+1]
  922 + markers = self.matrix[:, :, n]
  923 +
  924 +
  925 + ww = self.viewer.slice_.window_width
  926 + wl = self.viewer.slice_.window_level
  927 +
  928 + if BRUSH_BACKGROUND in markers and BRUSH_FOREGROUND in markers:
  929 + tmp_image = ndimage.morphological_gradient(get_LUT_value(image, ww, wl).astype('uint16'), self.mg_size)
  930 + tmp_mask = watershed(tmp_image, markers)
  931 +
  932 + if self.viewer.overwrite_mask:
  933 + mask[:] = 0
  934 + mask[tmp_mask == 1] = 253
  935 + else:
  936 + mask[(tmp_mask==2) & ((mask == 0) | (mask == 2) | (mask == 253))] = 2
  937 + mask[(tmp_mask==1) & ((mask == 0) | (mask == 2) | (mask == 253))] = 253
  938 +
  939 +
  940 + self.viewer.slice_.current_mask.was_edited = True
  941 + self.viewer.slice_.current_mask.clear_history()
  942 + Publisher.sendMessage('Reload actual slice')
  943 + else:
  944 + self.viewer.OnScrollBar(update3D=False)
  945 +
  946 + def get_coordinate_cursor(self):
  947 + # Find position
  948 + x, y, z = self.picker.GetPickPosition()
  949 + bounds = self.viewer.slice_data.actor.GetBounds()
  950 + if bounds[0] == bounds[1]:
  951 + x = bounds[0]
  952 + elif bounds[2] == bounds[3]:
  953 + y = bounds[2]
  954 + elif bounds[4] == bounds[5]:
  955 + z = bounds[4]
  956 + return x, y, z
  957 +
  958 + def edit_mask_pixel(self, operation, n, index, position, radius, orientation):
  959 + if orientation == 'AXIAL':
  960 + mask = self.matrix[n, :, :]
  961 + elif orientation == 'CORONAL':
  962 + mask = self.matrix[:, n, :]
  963 + elif orientation == 'SAGITAL':
  964 + mask = self.matrix[:, :, n]
  965 +
  966 + spacing = self.viewer.slice_.spacing
  967 + if hasattr(position, '__iter__'):
  968 + py, px = position
  969 + if orientation == 'AXIAL':
  970 + sx = spacing[0]
  971 + sy = spacing[1]
  972 + elif orientation == 'CORONAL':
  973 + sx = spacing[0]
  974 + sy = spacing[2]
  975 + elif orientation == 'SAGITAL':
  976 + sx = spacing[2]
  977 + sy = spacing[1]
  978 +
  979 + else:
  980 + if orientation == 'AXIAL':
  981 + sx = spacing[0]
  982 + sy = spacing[1]
  983 + py = position / mask.shape[1]
  984 + px = position % mask.shape[1]
  985 + elif orientation == 'CORONAL':
  986 + sx = spacing[0]
  987 + sy = spacing[2]
  988 + py = position / mask.shape[1]
  989 + px = position % mask.shape[1]
  990 + elif orientation == 'SAGITAL':
  991 + sx = spacing[2]
  992 + sy = spacing[1]
  993 + py = position / mask.shape[1]
  994 + px = position % mask.shape[1]
  995 +
  996 + cx = index.shape[1] / 2 + 1
  997 + cy = index.shape[0] / 2 + 1
  998 + xi = px - index.shape[1] + cx
  999 + xf = xi + index.shape[1]
  1000 + yi = py - index.shape[0] + cy
  1001 + yf = yi + index.shape[0]
  1002 +
  1003 + if yi < 0:
  1004 + index = index[abs(yi):,:]
  1005 + yi = 0
  1006 + if yf > mask.shape[0]:
  1007 + index = index[:index.shape[0]-(yf-mask.shape[0]), :]
  1008 + yf = mask.shape[0]
  1009 +
  1010 + if xi < 0:
  1011 + index = index[:,abs(xi):]
  1012 + xi = 0
  1013 + if xf > mask.shape[1]:
  1014 + index = index[:,:index.shape[1]-(xf-mask.shape[1])]
  1015 + xf = mask.shape[1]
  1016 +
  1017 + # Verifying if the points is over the image array.
  1018 + if (not 0 <= xi <= mask.shape[1] and not 0 <= xf <= mask.shape[1]) or \
  1019 + (not 0 <= yi <= mask.shape[0] and not 0 <= yf <= mask.shape[0]):
  1020 + return
  1021 +
  1022 + roi_m = mask[yi:yf,xi:xf]
  1023 +
  1024 + # Checking if roi_i has at least one element.
  1025 + if roi_m.size:
  1026 + roi_m[index] = operation
  1027 +
  1028 + def expand_watershed(self, pubsub_evt):
  1029 + markers = self.matrix
  1030 + image = self.viewer.slice_.matrix
  1031 + mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:]
  1032 + ww = self.viewer.slice_.window_width
  1033 + wl = self.viewer.slice_.window_level
  1034 + if BRUSH_BACKGROUND in markers and BRUSH_FOREGROUND in markers:
  1035 + tmp_image = ndimage.morphological_gradient(get_LUT_value(image, ww, wl).astype('uint16'), self.mg_size)
  1036 + tmp_mask = watershed(tmp_image, markers)
  1037 +
  1038 + if self.viewer.overwrite_mask:
  1039 + mask[:] = 0
  1040 + mask[tmp_mask == 1] = 253
  1041 + else:
  1042 + mask[(tmp_mask==2) & ((mask == 0) | (mask == 2) | (mask == 253))] = 2
  1043 + mask[(tmp_mask==1) & ((mask == 0) | (mask == 2) | (mask == 253))] = 253
  1044 +
  1045 + #mask[:] = tmp_mask
  1046 + self.viewer.slice_.current_mask.matrix[0] = 1
  1047 + self.viewer.slice_.current_mask.matrix[:, 0, :] = 1
  1048 + self.viewer.slice_.current_mask.matrix[:, :, 0] = 1
  1049 +
  1050 + self.viewer.slice_.discard_all_buffers()
  1051 + self.viewer.slice_.current_mask.clear_history()
  1052 + Publisher.sendMessage('Reload actual slice')
  1053 +
  1054 +
643 1055 def get_style(style):
644 1056 STYLES = {
645 1057 const.STATE_DEFAULT: DefaultInteractorStyle,
... ... @@ -653,5 +1065,6 @@ def get_style(style):
653 1065 const.STATE_ZOOM_SL: ZoomSLInteractorStyle,
654 1066 const.SLICE_STATE_SCROLL: ChangeSliceInteractorStyle,
655 1067 const.SLICE_STATE_EDITOR: EditorInteractorStyle,
  1068 + const.SLICE_STATE_WATERSHED: WaterShedInteractorStyle,
656 1069 }
657 1070 return STYLES[style]
... ...
invesalius/data/viewer_slice.py
... ... @@ -166,6 +166,8 @@ class Viewer(wx.Panel):
166 166 self.last_position_mouse_move = ()
167 167 self.state = const.STATE_DEFAULT
168 168  
  169 + self.overwrite_mask = False
  170 +
169 171 # All renderers and image actors in this viewer
170 172 self.slice_data_list = []
171 173 self.slice_data = None
... ... @@ -281,6 +283,8 @@ class Viewer(wx.Panel):
281 283 if cleanup:
282 284 self.style.CleanUp()
283 285  
  286 + del self.style
  287 +
284 288 style = styles.get_style(state)(self)
285 289  
286 290 setup = getattr(style, 'SetUp', None)
... ... @@ -751,6 +755,8 @@ class Viewer(wx.Panel):
751 755 Publisher.subscribe(self.OnSetMIPInvert, 'Set MIP Invert %s' % self.orientation)
752 756 Publisher.subscribe(self.OnShowMIPInterface, 'Show MIP interface')
753 757  
  758 + Publisher.subscribe(self.OnSetOverwriteMask, "Set overwrite mask")
  759 +
754 760 def SetDefaultCursor(self, pusub_evt):
755 761 self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
756 762  
... ... @@ -1247,7 +1253,10 @@ class Viewer(wx.Panel):
1247 1253 self.mip_ctrls.Hide()
1248 1254 self.GetSizer().Remove(self.mip_ctrls)
1249 1255 self.Layout()
1250   -
  1256 +
  1257 + def OnSetOverwriteMask(self, pubsub_evt):
  1258 + value = pubsub_evt.data
  1259 + self.overwrite_mask = value
1251 1260  
1252 1261 def set_slice_number(self, index):
1253 1262 inverted = self.mip_ctrls.inverted.GetValue()
... ...
invesalius/gui/frame.py
... ... @@ -26,6 +26,8 @@ import webbrowser
26 26 import wx
27 27 import wx.aui
28 28 from wx.lib.pubsub import pub as Publisher
  29 +import wx.lib.agw.toasterbox as TB
  30 +import wx.lib.popupctl as pc
29 31  
30 32 import constants as const
31 33 import default_tasks as tasks
... ... @@ -45,6 +47,19 @@ VIEW_TOOLS = [ID_LAYOUT, ID_TEXT] =\
45 47  
46 48  
47 49  
  50 +class MessageWatershed(wx.PopupWindow):
  51 + def __init__(self, prnt, msg):
  52 + wx.PopupWindow.__init__(self, prnt, -1)
  53 + self.txt = wx.StaticText(self, -1, msg)
  54 +
  55 + self.sizer = wx.BoxSizer(wx.HORIZONTAL)
  56 + self.sizer.Add(self.txt, 1, wx.EXPAND)
  57 + self.SetSizer(self.sizer)
  58 +
  59 + self.sizer.Fit(self)
  60 + self.Layout()
  61 + self.Update()
  62 + self.SetAutoLayout(1)
48 63  
49 64  
50 65  
... ... @@ -64,6 +79,8 @@ class Frame(wx.Frame):
64 79 self.Center(wx.BOTH)
65 80 icon_path = os.path.join(const.ICON_DIR, "invesalius.ico")
66 81 self.SetIcon(wx.Icon(icon_path, wx.BITMAP_TYPE_ICO))
  82 +
  83 + self.mw = None
67 84  
68 85 if sys.platform != 'darwin':
69 86 self.Maximize()
... ... @@ -104,6 +121,7 @@ class Frame(wx.Frame):
104 121 sub(self._SetProjectName, 'Set project name')
105 122 sub(self._ShowContentPanel, 'Show content panel')
106 123 sub(self._ShowImportPanel, 'Show import panel in frame')
  124 + #sub(self._ShowHelpMessage, 'Show help message')
107 125 sub(self._ShowImportNetwork, 'Show retrieve dicom panel')
108 126 sub(self._ShowTask, 'Show task panel')
109 127 sub(self._UpdateAUI, 'Update AUI')
... ... @@ -116,6 +134,7 @@ class Frame(wx.Frame):
116 134 self.Bind(wx.EVT_SIZE, self.OnSize)
117 135 self.Bind(wx.EVT_MENU, self.OnMenuClick)
118 136 self.Bind(wx.EVT_CLOSE, self.OnClose)
  137 + #self.Bind(wx.EVT_MOVE, self.OnMove)
119 138  
120 139 def __init_aui(self):
121 140 """
... ... @@ -289,6 +308,14 @@ class Frame(wx.Frame):
289 308 aui_manager.GetPane("Import").Show(0)
290 309 aui_manager.Update()
291 310  
  311 + def _ShowHelpMessage(self, evt_pubsub):
  312 + aui_manager = self.aui_manager
  313 + pos = aui_manager.GetPane("Data").window.GetScreenPosition()
  314 + msg = evt_pubsub.data
  315 + self.mw = MessageWatershed(self, msg)
  316 + self.mw.SetPosition(pos)
  317 + self.mw.Show()
  318 +
292 319 def _ShowImportPanel(self, evt_pubsub):
293 320 """
294 321 Show only DICOM import panel.
... ... @@ -378,6 +405,12 @@ class Frame(wx.Frame):
378 405 Publisher.sendMessage(('ProgressBar Reposition'))
379 406 evt.Skip()
380 407  
  408 +
  409 + def OnMove(self, evt):
  410 + aui_manager = self.aui_manager
  411 + pos = aui_manager.GetPane("Data").window.GetScreenPosition()
  412 + self.mw.SetPosition(pos)
  413 +
381 414 def ShowPreferences(self):
382 415  
383 416 if self.preferences.ShowModal() == wx.ID_OK:
... ...
invesalius/gui/task_slice.py
... ... @@ -228,7 +228,7 @@ class InnerFoldPanel(wx.Panel):
228 228 # parent panel. Perhaps we need to insert the item into the sizer also...
229 229 # Study this.
230 230 fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition,
231   - (10, 190), 0,fpb.FPB_SINGLE_FOLD)
  231 + (10, 220), 0,fpb.FPB_SINGLE_FOLD)
232 232  
233 233 # Fold panel style
234 234 style = fpb.CaptionBarStyle()
... ... @@ -252,6 +252,13 @@ class InnerFoldPanel(wx.Panel):
252 252 self.__id_editor = item.GetId()
253 253 self.last_panel_opened = None
254 254  
  255 + # Fold 3 - Watershed
  256 + item = fold_panel.AddFoldPanel(_("Watershed"), collapsed=True)
  257 + fold_panel.ApplyCaptionStyle(item, style)
  258 + fold_panel.AddFoldPanelWindow(item, WatershedTool(item), Spacing= 0,
  259 + leftSpacing=0, rightSpacing=0)
  260 + self.__id_watershed = item.GetId()
  261 +
255 262 #fold_panel.Expand(fold_panel.GetFoldPanel(1))
256 263  
257 264 # Panel sizer to expand fold panel
... ... @@ -274,6 +281,7 @@ class InnerFoldPanel(wx.Panel):
274 281 def __bind_pubsub_evt(self):
275 282 Publisher.subscribe(self.OnRetrieveStyle, 'Retrieve task slice style')
276 283 Publisher.subscribe(self.OnDisableStyle, 'Disable task slice style')
  284 + Publisher.subscribe(self.OnCloseProject, 'Close project data')
277 285  
278 286 def OnFoldPressCaption(self, evt):
279 287 id = evt.GetTag().GetId()
... ... @@ -284,8 +292,18 @@ class InnerFoldPanel(wx.Panel):
284 292 Publisher.sendMessage('Disable style', const.SLICE_STATE_EDITOR)
285 293 self.last_style = None
286 294 else:
287   - Publisher.sendMessage('Enable style', const.SLICE_STATE_EDITOR)
  295 + Publisher.sendMessage('Enable style',
  296 + const.SLICE_STATE_EDITOR)
288 297 self.last_style = const.SLICE_STATE_EDITOR
  298 + elif self.__id_watershed == id:
  299 + if closed:
  300 + Publisher.sendMessage('Disable style',
  301 + const.SLICE_STATE_WATERSHED)
  302 + self.last_style = None
  303 + else:
  304 + Publisher.sendMessage('Enable style', const.SLICE_STATE_WATERSHED)
  305 + Publisher.sendMessage('Show help message', 'Mark the object and the background')
  306 + self.last_style = const.SLICE_STATE_WATERSHED
289 307 else:
290 308 Publisher.sendMessage('Disable style', const.SLICE_STATE_EDITOR)
291 309 self.last_style = None
... ... @@ -300,6 +318,9 @@ class InnerFoldPanel(wx.Panel):
300 318 if (self.last_style == const.SLICE_STATE_EDITOR):
301 319 Publisher.sendMessage('Disable style', const.SLICE_STATE_EDITOR)
302 320  
  321 + def OnCloseProject(self, pubsub_evt):
  322 + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0))
  323 +
303 324 def GetMaskSelected(self):
304 325 x= self.mask_prop_panel.GetMaskSelected()
305 326 return self.mask_prop_panel.GetMaskSelected()
... ... @@ -688,3 +709,143 @@ class EditionTools(wx.Panel):
688 709 Publisher.sendMessage('Set edition operation', brush_op_id)
689 710  
690 711  
  712 +class WatershedTool(EditionTools):
  713 + def __init__(self, parent):
  714 + wx.Panel.__init__(self, parent, size=(50,150))
  715 + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR)
  716 + self.SetBackgroundColour(default_colour)
  717 +
  718 + ## LINE 1
  719 + text1 = wx.StaticText(self, -1, _("Choose brush type and size:"))
  720 +
  721 + ## LINE 2
  722 + menu = wx.Menu()
  723 +
  724 + CIRCLE_BMP = wx.Bitmap("../icons/brush_circle.jpg", wx.BITMAP_TYPE_JPEG)
  725 + item = wx.MenuItem(menu, MENU_BRUSH_CIRCLE, _("Circle"))
  726 + item.SetBitmap(CIRCLE_BMP)
  727 +
  728 + SQUARE_BMP = wx.Bitmap("../icons/brush_square.jpg", wx.BITMAP_TYPE_JPEG)
  729 + item2 = wx.MenuItem(menu, MENU_BRUSH_SQUARE, _("Square"))
  730 + item2.SetBitmap(SQUARE_BMP)
  731 +
  732 + menu.AppendItem(item)
  733 + menu.AppendItem(item2)
  734 +
  735 + bmp_brush_format = {const.BRUSH_CIRCLE: CIRCLE_BMP,
  736 + const.BRUSH_SQUARE: SQUARE_BMP}
  737 + selected_bmp = bmp_brush_format[const.DEFAULT_BRUSH_FORMAT]
  738 +
  739 + btn_brush_format = pbtn.PlateButton(self, wx.ID_ANY,"", selected_bmp,
  740 + style=pbtn.PB_STYLE_SQUARE)
  741 + btn_brush_format.SetMenu(menu)
  742 + self.btn_brush_format = btn_brush_format
  743 +
  744 + spin_brush_size = wx.SpinCtrl(self, -1, "", (20, 50))
  745 + spin_brush_size.SetRange(1,100)
  746 + spin_brush_size.SetValue(const.BRUSH_SIZE)
  747 + spin_brush_size.Bind(wx.EVT_TEXT, self.OnBrushSize)
  748 + self.spin = spin_brush_size
  749 +
  750 + combo_brush_op = wx.ComboBox(self, -1, "", size=(15,-1),
  751 + choices = (_("Foreground"),
  752 + _("Background"),
  753 + _("Erase")),
  754 + style = wx.CB_DROPDOWN|wx.CB_READONLY)
  755 + combo_brush_op.SetSelection(0)
  756 + if sys.platform != 'win32':
  757 + combo_brush_op.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
  758 + self.combo_brush_op = combo_brush_op
  759 +
  760 + # Sizer which represents the second line
  761 + line2 = wx.BoxSizer(wx.HORIZONTAL)
  762 + line2.Add(btn_brush_format, 0, wx.EXPAND|wx.GROW|wx.TOP|wx.RIGHT, 0)
  763 + line2.Add(spin_brush_size, 0, wx.RIGHT, 5)
  764 + line2.Add(combo_brush_op, 1, wx.EXPAND|wx.TOP|wx.RIGHT|wx.LEFT, 5)
  765 +
  766 + ## LINE 3
  767 +
  768 + ## LINE 4
  769 +
  770 + # LINE 5
  771 + check_box = wx.CheckBox(self, -1, _("Overwrite mask"))
  772 + self.check_box = check_box
  773 +
  774 + # Line 6
  775 + self.btn_exp_watershed = wx.Button(self, -1, _('Expand watershed to 3D'))
  776 +
  777 + # Add lines into main sizer
  778 + sizer = wx.BoxSizer(wx.VERTICAL)
  779 + sizer.Add(text1, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5)
  780 + sizer.Add(line2, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5)
  781 + sizer.Add(check_box, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5)
  782 + sizer.Add(self.btn_exp_watershed, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP, 5)
  783 + sizer.Fit(self)
  784 +
  785 + self.SetSizer(sizer)
  786 + self.Update()
  787 + self.SetAutoLayout(1)
  788 +
  789 + self.__bind_events_wx()
  790 +
  791 +
  792 + def __bind_events_wx(self):
  793 + self.Bind(wx.EVT_MENU, self.OnMenu)
  794 + self.combo_brush_op.Bind(wx.EVT_COMBOBOX, self.OnComboBrushOp)
  795 + self.check_box.Bind(wx.EVT_CHECKBOX, self.OnCheckOverwriteMask)
  796 + self.btn_exp_watershed.Bind(wx.EVT_BUTTON, self.OnExpandWatershed)
  797 +
  798 + def ChangeMaskColour(self, pubsub_evt):
  799 + colour = pubsub_evt.data
  800 + self.gradient_thresh.SetColour(colour)
  801 +
  802 + def SetGradientColour(self, pubsub_evt):
  803 + vtk_colour = pubsub_evt.data[3]
  804 + wx_colour = [c*255 for c in vtk_colour]
  805 + self.gradient_thresh.SetColour(wx_colour)
  806 +
  807 + def SetThresholdValues(self, pubsub_evt):
  808 + thresh_min, thresh_max = pubsub_evt.data
  809 + self.bind_evt_gradient = False
  810 + self.gradient_thresh.SetMinValue(thresh_min)
  811 + self.gradient_thresh.SetMaxValue(thresh_max)
  812 + self.bind_evt_gradient = True
  813 +
  814 + def SetThresholdBounds(self, pubsub_evt):
  815 + thresh_min = pubsub_evt.data[0]
  816 + thresh_max = pubsub_evt.data[1]
  817 + self.gradient_thresh.SetMinRange(thresh_min)
  818 + self.gradient_thresh.SetMaxRange(thresh_max)
  819 + self.gradient_thresh.SetMinValue(thresh_min)
  820 + self.gradient_thresh.SetMaxValue(thresh_max)
  821 +
  822 + def OnMenu(self, evt):
  823 + SQUARE_BMP = wx.Bitmap("../icons/brush_square.jpg", wx.BITMAP_TYPE_JPEG)
  824 + CIRCLE_BMP = wx.Bitmap("../icons/brush_circle.jpg", wx.BITMAP_TYPE_JPEG)
  825 +
  826 + brush = {MENU_BRUSH_CIRCLE: const.BRUSH_CIRCLE,
  827 + MENU_BRUSH_SQUARE: const.BRUSH_SQUARE}
  828 + bitmap = {MENU_BRUSH_CIRCLE: CIRCLE_BMP,
  829 + MENU_BRUSH_SQUARE: SQUARE_BMP}
  830 +
  831 + self.btn_brush_format.SetBitmap(bitmap[evt.GetId()])
  832 +
  833 + Publisher.sendMessage('Set brush format', brush[evt.GetId()])
  834 +
  835 + def OnBrushSize(self, evt):
  836 + """ """
  837 + # FIXME: Using wx.EVT_SPINCTRL in MacOS it doesnt capture changes only
  838 + # in the text ctrl - so we are capturing only changes on text
  839 + # Strangelly this is being called twice
  840 + Publisher.sendMessage('Set edition brush size',self.spin.GetValue())
  841 +
  842 + def OnComboBrushOp(self, evt):
  843 + brush_op = self.combo_brush_op.GetValue()
  844 + Publisher.sendMessage('Set watershed operation', brush_op)
  845 +
  846 + def OnCheckOverwriteMask(self, evt):
  847 + value = self.check_box.GetValue()
  848 + Publisher.sendMessage('Set overwrite mask', value)
  849 +
  850 + def OnExpandWatershed(self, evt):
  851 + Publisher.sendMessage('Expand watershed to 3D AXIAL')
... ...