diff --git a/icons/bool_difference.png b/icons/bool_difference.png new file mode 100644 index 0000000..cb61b3d Binary files /dev/null and b/icons/bool_difference.png differ diff --git a/icons/bool_disjunction.png b/icons/bool_disjunction.png new file mode 100644 index 0000000..f92b4b2 Binary files /dev/null and b/icons/bool_disjunction.png differ diff --git a/icons/bool_intersection.png b/icons/bool_intersection.png new file mode 100644 index 0000000..2b60e65 Binary files /dev/null and b/icons/bool_intersection.png differ diff --git a/icons/bool_union.png b/icons/bool_union.png new file mode 100644 index 0000000..4d26922 Binary files /dev/null and b/icons/bool_union.png differ diff --git a/invesalius/constants.py b/invesalius/constants.py index 6fcdc48..4651b65 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -475,6 +475,8 @@ ID_SWAP_XY = wx.NewId() ID_SWAP_XZ = wx.NewId() ID_SWAP_YZ = wx.NewId() +ID_BOOLEAN_MASK = wx.NewId() + #--------------------------------------------------------- STATE_DEFAULT = 1000 STATE_WL = 1001 @@ -565,3 +567,9 @@ PROJECTION_CONTOUR_MIDA=8 #------------ Projections defaults ------------------ PROJECTION_BORDER_SIZE=1.0 PROJECTION_MIP_SIZE=2 + +# ------------- Boolean operations ------------------ +BOOLEAN_UNION = 1 +BOOLEAN_DIFF = 2 +BOOLEAN_AND = 3 +BOOLEAN_XOR = 4 diff --git a/invesalius/control.py b/invesalius/control.py index d27e7d1..c9c62a1 100644 --- a/invesalius/control.py +++ b/invesalius/control.py @@ -82,6 +82,8 @@ class Controller(): Publisher.subscribe(self.OnOpenRecentProject, 'Open recent project') Publisher.subscribe(self.OnShowAnalyzeFile, 'Show analyze dialog') + Publisher.subscribe(self.ShowBooleanOpDialog, 'Show boolean dialog') + def OnCancelImport(self, pubsub_evt): #self.cancel_import = True @@ -628,6 +630,6 @@ class Controller(): preset_name + '.plist') plistlib.writePlist(preset, preset_dir) - - - + def ShowBooleanOpDialog(self, pubsub_evt): + dlg = dialogs.MaskBooleanDialog(prj.Project().mask_dict) + dlg.ShowModal() diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index 106fb10..9f73e5d 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -160,6 +160,8 @@ class Slice(object): Publisher.subscribe(self._set_projection_type, 'Set projection type') + Publisher.subscribe(self._do_boolean_op, 'Do boolean operation') + Publisher.subscribe(self.OnExportMask,'Export mask to file') Publisher.subscribe(self.OnCloseProject, 'Close project data') @@ -1080,12 +1082,17 @@ class Slice(object): else: node.value += shiftWW * factor - def do_threshold_to_a_slice(self, slice_matrix, mask): + def do_threshold_to_a_slice(self, slice_matrix, mask, threshold=None): """ Based on the current threshold bounds generates a threshold mask to given slice_matrix. """ - thresh_min, thresh_max = self.current_mask.threshold_range + if threshold: + thresh_min, thresh_max = threshold + else: + thresh_min, thresh_max = self.current_mask.threshold_range + + print ">>>> THreshold", thresh_min, thresh_max m = (((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255) m[mask == 1] = 1 m[mask == 2] = 2 @@ -1106,7 +1113,7 @@ class Slice(object): 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[n, 1:, 1:] = self.do_threshold_to_a_slice(self.matrix[n-1], m, mask.threshold_range) mask.matrix.flush() @@ -1209,6 +1216,51 @@ class Slice(object): return blend_imagedata.GetOutput() + def _do_boolean_op(self, pubsub_evt): + op, m1, m2 = pubsub_evt.data + self.do_boolean_op(op, m1, m2) + + def do_boolean_op(self, op, m1, m2): + name_ops = {const.BOOLEAN_UNION: _(u"Union"), + const.BOOLEAN_DIFF: _(u"Diff"), + const.BOOLEAN_AND: _(u"Intersection"), + const.BOOLEAN_XOR: _(u"XOR")} + + + name = u"%s_%s_%s" % (name_ops[op], m1.name, m2.name) + proj = Project() + mask_dict = proj.mask_dict + names_list = [mask_dict[i].name for i in mask_dict.keys()] + new_name = utils.next_copy_name(name, names_list) + + future_mask = Mask() + future_mask.create_mask(self.matrix.shape) + future_mask.name = new_name + + future_mask.matrix[:] = 1 + m = future_mask.matrix[1:, 1:, 1:] + + self.do_threshold_to_all_slices(m1) + m1 = m1.matrix[1:, 1:, 1:] + + self.do_threshold_to_all_slices(m2) + m2 = m2.matrix[1:, 1:, 1:] + + if op == const.BOOLEAN_UNION: + m[:] = ((m1 > 2) + (m2 > 2)) * 255 + + elif op == const.BOOLEAN_DIFF: + m[:] = ((m1 > 2) - ((m1 > 2) & (m2 > 2))) * 255 + + elif op == const.BOOLEAN_AND: + m[:] = ((m1 > 2) & (m2 > 2)) * 255 + + elif op == const.BOOLEAN_XOR: + m[:] = numpy.logical_xor((m1 > 2), (m2 > 2)) * 255 + + future_mask.was_edited = True + self._add_mask_into_proj(future_mask) + def apply_slice_buffer_to_mask(self, orientation): """ Apply the modifications (edition) in mask buffer to mask. diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 949d35b..f297b23 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -23,6 +23,7 @@ import random import sys import wx +import wx.combo from wx.lib import masked from wx.lib.agw import floatspin from wx.lib.wordwrap import wordwrap @@ -1469,3 +1470,82 @@ class WatershedOptionsDialog(wx.Dialog): self.SetSizer(sizer) sizer.Fit(self) self.Layout() + + +class MaskBooleanDialog(wx.Dialog): + def __init__(self, masks): + pre = wx.PreDialog() + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Booleans operations"), style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) + self.PostCreate(pre) + + self._init_gui(masks) + + def _init_gui(self, masks): + mask_choices = [(masks[i].name, masks[i]) for i in sorted(masks)] + self.mask1 = wx.ComboBox(self, -1, mask_choices[0][0], choices=[]) + self.mask2 = wx.ComboBox(self, -1, mask_choices[0][0], choices=[]) + + for n, m in mask_choices: + self.mask1.Append(n, m) + self.mask2.Append(n, m) + + self.mask1.SetSelection(0) + + if len(mask_choices) > 1: + self.mask2.SetSelection(1) + else: + self.mask2.SetSelection(0) + + icon_folder = '../icons/' + op_choices = ((_(u"Union"), const.BOOLEAN_UNION, 'bool_union.png'), + (_(u"Difference"), const.BOOLEAN_DIFF, 'bool_difference.png'), + (_(u"Intersection"), const.BOOLEAN_AND, 'bool_intersection.png'), + (_(u"Exclusive disjunction"), const.BOOLEAN_XOR, 'bool_disjunction.png')) + self.op_boolean = wx.combo.BitmapComboBox(self, -1, op_choices[0][0], choices=[]) + + for n, i, f in op_choices: + bmp = wx.Bitmap(os.path.join(icon_folder, f), wx.BITMAP_TYPE_PNG) + self.op_boolean.Append(n, bmp, i) + + self.op_boolean.SetSelection(0) + + btn_ok = wx.Button(self, wx.ID_OK) + btn_ok.SetDefault() + + btn_cancel = wx.Button(self, wx.ID_CANCEL) + + btnsizer = wx.StdDialogButtonSizer() + btnsizer.AddButton(btn_ok) + btnsizer.AddButton(btn_cancel) + btnsizer.Realize() + + gsizer = wx.FlexGridSizer(rows=3, cols=2, hgap=5, vgap=5) + + gsizer.Add(wx.StaticText(self, -1, _(u"Mask 1"))) + gsizer.Add(self.mask1, 1, wx.EXPAND) + gsizer.Add(wx.StaticText(self, -1, _(u"Operation"))) + gsizer.Add(self.op_boolean, 1, wx.EXPAND) + gsizer.Add(wx.StaticText(self, -1, _(u"Mask 2"))) + gsizer.Add(self.mask2, 1, wx.EXPAND) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(gsizer, 0, wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5) + sizer.Add(btnsizer, 0, wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5) + + self.SetSizer(sizer) + sizer.Fit(self) + + self.Centre() + + btn_ok.Bind(wx.EVT_BUTTON, self.OnOk) + + def OnOk(self, evt): + op = self.op_boolean.GetClientData(self.op_boolean.GetSelection()) + m1 = self.mask1.GetClientData(self.mask1.GetSelection()) + m2 = self.mask2.GetClientData(self.mask2.GetSelection()) + + Publisher.sendMessage('Do boolean operation', (op, m1, m2)) + Publisher.sendMessage('Reload actual slice') + + self.Close() + self.Destroy() diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 24ab96c..2654ef4 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -397,6 +397,9 @@ class Frame(wx.Frame): elif id == wx.ID_REDO: self.OnRedo() + elif id == const.ID_BOOLEAN_MASK: + self.OnMaskBoolean() + def OnSize(self, evt): """ Refresh GUI when frame is resized. @@ -490,10 +493,11 @@ class Frame(wx.Frame): Publisher.sendMessage('Redo edition') + def OnMaskBoolean(self): + print "Mask boolean" + Publisher.sendMessage('Show boolean dialog') - - # ------------------------------------------------------------------ # ------------------------------------------------------------------ # ------------------------------------------------------------------ @@ -605,6 +609,11 @@ class MenuBar(wx.MenuBar): #app(const.ID_EDIT_LIST, "Show Undo List...") ################################################################# + # Mask Menu + mask_menu = wx.Menu() + mask_menu.Append(const.ID_BOOLEAN_MASK, _(u"Boolean operations")) + file_edit.AppendMenu(-1, _(u"Mask"), mask_menu) + # VIEW #view_tool_menu = wx.Menu() -- libgit2 0.21.2