Commit c180da8d50612b70e749b30d35a8f881d0453397
1 parent
9c8083ac
Exists in
master
and in
52 other branches
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
Showing
7 changed files
with
722 additions
and
8 deletions
Show diff stats
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') | ... | ... |